Added UserAction to API and Chat
parent
d774f2f93c
commit
5b85fabcbd
|
|
@ -166,3 +166,6 @@ staticfiles/
|
|||
|
||||
# database
|
||||
db.sqlite3
|
||||
|
||||
# todo
|
||||
todo*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
from .views import *
|
||||
from users.models import *
|
||||
|
||||
def is_action_processable(request):
|
||||
user = Users.objects.get(request.body["user"])
|
||||
if user:
|
||||
# TODO implement
|
||||
return True
|
||||
|
||||
|
||||
def process_action(request):
|
||||
if not is_action_processable(request):
|
||||
return
|
||||
print(request.body)
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Defines Data ffs
|
||||
|
||||
|
||||
VIEW_ACTION = 0
|
||||
PURCHASE_ACTION = 1
|
||||
MODIFY_ACTION = 2
|
||||
EQUIP_ACTION = 3
|
||||
|
||||
# Need to define costs for purchase actions and requirements for modify and equip actions
|
||||
|
||||
# Saving space and reducing response times of the server is a priority so keeping in mind that this
|
||||
# data will probably live on the RAM (as file-io is costly) so it needs to be small and easy to parse
|
||||
|
||||
# # sights,trg,brl
|
||||
# # cost,ammo^inter^ >^skin,number
|
||||
# # |----|^^|-||-|>^>^|--||------|
|
||||
# WEAPON_DATA = """000000000000000000000000000000""" # Use Ints goddamit
|
||||
# oh yeah, ints
|
||||
WEAPON_DATA = [] # wtf
|
||||
69
api/tests.py
69
api/tests.py
|
|
@ -1,3 +1,72 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
from rest_framework import status
|
||||
|
||||
# Create your tests here.
|
||||
class UnAuthAccessTests(APITestCase):
|
||||
def test_province_get(self):
|
||||
response = self.client.get("/api/v1/provinces/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_assault_troops_get(self):
|
||||
response = self.client.get("/api/v1/assault_troops/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_servers_get(self):
|
||||
response = self.client.get("/api/v1/servers/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_players_get(self):
|
||||
response = self.client.get("/api/v1/players/")
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_user_action_get(self):
|
||||
response = self.client.get("/api/v1/user_action/")
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_user_action_post(self):
|
||||
response = self.client.post("/api/v1/user_action/", {"user": 1, "action": "142"}, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
class AuthAccessTests(APITestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="test", password="pass123")
|
||||
self.user2 = User.objects.create_user(username="test2", password="pass123", is_staff=True)
|
||||
|
||||
def test_province_get(self):
|
||||
self.client.login(username="test", password="pass123")
|
||||
response = self.client.get("/api/v1/provinces/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_assault_troops_get(self):
|
||||
self.client.login(username="test", password="pass123")
|
||||
response = self.client.get("/api/v1/assault_troops/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_servers_get(self):
|
||||
self.client.login(username="test", password="pass123")
|
||||
response = self.client.get("/api/v1/servers/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_players_get(self):
|
||||
self.client.login(username="test", password="pass123")
|
||||
response = self.client.get("/api/v1/players/")
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_players_staff_get(self):
|
||||
self.client.login(username="test2", password="pass123")
|
||||
response = self.client.get("/api/v1/players/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_user_action_post(self):
|
||||
self.client.login(username="test", password="pass123")
|
||||
response = self.client.post("/api/v1/user_action/", {"user": 1, "action": "142"}, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_user_data_get(self):
|
||||
self.client.login(username="test", password="pass123")
|
||||
response = self.client.get("/api/v1/user_data/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,4 +12,5 @@ urlpatterns = [
|
|||
path('servers/<int:nm>/', ServerView.as_view()),
|
||||
path('user_data/', UserDataView.as_view()),
|
||||
path('user_data/<int:pk>/', UserDatumView.as_view()),
|
||||
path('user_action/', UserActionView.as_view()),
|
||||
]
|
||||
|
|
|
|||
16
api/views.py
16
api/views.py
|
|
@ -1,8 +1,9 @@
|
|||
from rest_framework import generics, permissions
|
||||
from rest_framework import generics, permissions, mixins
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from .models import *
|
||||
from .filters import *
|
||||
from .actions import *
|
||||
from .serializers import *
|
||||
from .permissions import *
|
||||
from users.models import *
|
||||
|
|
@ -42,7 +43,7 @@ class PlayersView(generics.ListCreateAPIView):
|
|||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class PlayerView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = (IsStaff)
|
||||
permission_classes = (IsStaff,)
|
||||
queryset = Player.objects.all()
|
||||
serializer_class = PlayerSerializer
|
||||
|
||||
|
|
@ -73,3 +74,14 @@ class UserDataView(generics.ListCreateAPIView):
|
|||
queryset = UserData.objects.all()
|
||||
serializer_class = UserDataSerializer
|
||||
filter_backends = [UserDataFilterBackend]
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class UserActionView(mixins.CreateModelMixin, generics.GenericAPIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
queryset = UserAction.objects.all()
|
||||
serializer_class = UserActionSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
process_action(request)
|
||||
return self.create(request, *args, **kwargs)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from django import forms
|
||||
from .models import Post
|
||||
|
||||
|
||||
class PostForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = ['content']
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
# from django.utils import timezone
|
||||
|
||||
|
||||
# Create your models here.
|
||||
|
|
@ -8,7 +8,8 @@ class Blog(models.Model):
|
|||
content = models.TextField()
|
||||
title = models.CharField(max_length=150)
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
date_posted = models.DateTimeField(default=timezone.now)
|
||||
# date_posted = models.DateTimeField(default=timezone.now)
|
||||
date_posted = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return '/'
|
||||
|
|
@ -18,4 +19,4 @@ class Blog(models.Model):
|
|||
class Post(models.Model):
|
||||
content = models.TextField()
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
date_posted = models.DateTimeField(default=timezone.now)
|
||||
date_posted = models.DateTimeField(auto_now_add=True)
|
||||
|
|
|
|||
12
blog/urls.py
12
blog/urls.py
|
|
@ -4,10 +4,12 @@ from . import views
|
|||
urlpatterns = [
|
||||
path('', views.home, name='Home'),
|
||||
path('news/', views.news, name='News'),
|
||||
path('blog/<int:pk>', views.BlogDetailView.as_view(), name='Blog'),
|
||||
path('blog/create/', views.BlogCreateView.as_view(), name='Blog Create'),
|
||||
path('post/<int:pk>', views.PostDetailView.as_view(), name='Post'),
|
||||
path('post/create/', views.PostCreateView.as_view(), name='Post Create'),
|
||||
path('dev/', views.dev, name='Dev'),
|
||||
# path('blog/<int:pk>', views.BlogDetailView.as_view(), name='Blog'),
|
||||
# path('blog/create/', views.BlogCreateView.as_view(), name='Blog Create'),
|
||||
# path('post/<int:pk>', views.PostDetailView.as_view(), name='Post'),
|
||||
# path('post/create/', views.PostCreateView.as_view(), name='Post Create'),
|
||||
# path('dev/', views.dev, name='Dev'),
|
||||
path('chat/', views.chat, name='Chat'),
|
||||
path("chat/p/", views.posts_partial, name="ChatPartial"),
|
||||
path('dev/support/', views.support, name='Support'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ from django.shortcuts import render
|
|||
from django.contrib.auth.mixins import *
|
||||
from django.views.generic import *
|
||||
from .models import *
|
||||
from .forms import *
|
||||
|
||||
MAX_POSTS = 150
|
||||
MARGIN = 20
|
||||
|
||||
# Create your views here.
|
||||
def news(request):
|
||||
|
|
@ -49,7 +52,7 @@ class BlogCreateView(LoginRequiredMixin, CreateView):
|
|||
|
||||
def form_valid(self, form):
|
||||
form.instance.author = self.request.user
|
||||
return super().form_valid(form)
|
||||
return super().form_valid(form) and self.request.user.is_staff
|
||||
|
||||
|
||||
|
||||
|
|
@ -68,6 +71,39 @@ class PostCreateView(LoginRequiredMixin, CreateView):
|
|||
return super().form_valid(form)
|
||||
|
||||
|
||||
def chat(request):
|
||||
if request.method == "POST":
|
||||
form = PostForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
post = form.save(commit=False)
|
||||
post.author = request.user
|
||||
post.save()
|
||||
print(Post.objects.count())
|
||||
if Post.objects.count() > MAX_POSTS + MARGIN:
|
||||
qs = Post.objects.order_by("-date_posted")
|
||||
old_ids = qs.values_list("id", flat=True)[MAX_POSTS:]
|
||||
|
||||
if old_ids:
|
||||
Post.objects.filter(id__in=old_ids).delete()
|
||||
|
||||
return render(request, "blog/partials/post.html", {"post": post})
|
||||
|
||||
posts = Post.objects.select_related("author").order_by("-date_posted")
|
||||
form = PostForm()
|
||||
|
||||
return render(request, "blog/postList.html", {
|
||||
"posts": posts,
|
||||
"form": form
|
||||
})
|
||||
|
||||
def posts_partial(request):
|
||||
posts = Post.objects.select_related("author").order_by("-date_posted")[:40]
|
||||
|
||||
return render(request, "blog/partials/postList.html", {
|
||||
"posts": posts
|
||||
})
|
||||
|
||||
def dev(request):
|
||||
return render(request, 'dev.html', {'title': 'Development'})
|
||||
|
||||
|
|
|
|||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
|
@ -512,10 +512,11 @@
|
|||
<ul class="nav-links">
|
||||
{% if user.is_authenticated %}
|
||||
<li><a href="/#news-section">News</a></li>
|
||||
<li><a href="{% url 'Chat' %}">Chat</a></li>
|
||||
{% else %}
|
||||
<li><a href="/#hero-section">Home</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/#about-section">About</a></li>
|
||||
{% endif %}
|
||||
<li><a href="/dev/support">Support Us</a></li>
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
|
|
@ -547,6 +548,7 @@
|
|||
<!-- <a href="#" aria-label="Twitter"><i class="fab fa-twitter"></i></a> -->
|
||||
<a href="https://www.reddit.com/r/beyondheroes/" target="blank" aria-label="Reddit"><i class="fab fa-reddit"></i></a>
|
||||
</div>
|
||||
<p style="padding-top:10px">Beyond Heroes™ | 2026 All Rights Reserved.</p>
|
||||
</footer>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{% load markdown_extras %}
|
||||
<div class="message">
|
||||
<div class="message-author">{{ post.author.username }} <span style="font-size: 10px">{{ post.date_posted | date:"Y/m/d H:i" }}</span></div>
|
||||
<p class="message-content">{{ post.content | markdown | safe }}</p>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{% for post in posts reversed %}
|
||||
{% include "blog/partials/post.html" %}
|
||||
{% endfor %}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<br><br><br>
|
||||
|
||||
<h2 style="opacity: 80%; padding-left:40px">General Chat</h2>
|
||||
|
||||
<style>
|
||||
.chat-container{
|
||||
/* width:420px;*/
|
||||
height:470px;
|
||||
border-radius:12px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.messages{
|
||||
flex:1;
|
||||
overflow-y:auto;
|
||||
padding: 10px 40px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
}
|
||||
|
||||
.message{
|
||||
background:#334155;
|
||||
padding:2px 5px;
|
||||
border-radius:2px;
|
||||
}
|
||||
|
||||
.message-author{
|
||||
font-size:15px;
|
||||
padding: 0px 5px;
|
||||
opacity:0.7;
|
||||
/* margin-bottom:4px;*/
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
padding: 0px 5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.input-area{
|
||||
display:flex;
|
||||
height: 60px;
|
||||
padding:20px 40px;
|
||||
border-top:1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.input-area input{
|
||||
flex: 1;
|
||||
padding:10px;
|
||||
font-size: 30px;
|
||||
border-radius:8px;
|
||||
border:none;
|
||||
outline:none;
|
||||
background:#0f172a;
|
||||
color:white;
|
||||
}
|
||||
|
||||
.input-area button{
|
||||
/* flex: 1;*/
|
||||
margin-left:8px;
|
||||
padding:10px 14px;
|
||||
border:none;
|
||||
border-radius:8px;
|
||||
background:#3b82f6;
|
||||
color:white;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
footer {;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="chat-container">
|
||||
<div
|
||||
id="post-list"
|
||||
class="messages"
|
||||
hx-get="{% url 'ChatPartial' %}"
|
||||
hx-trigger="every 3s"
|
||||
hx-swap="innerHTML">
|
||||
{% include "blog/partials/postList.html" %}
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="input-area"
|
||||
id="post-form"
|
||||
hx-post="{% url 'Chat' %}"
|
||||
hx-target="#post-list"
|
||||
hx-swap="beforeend">
|
||||
{% csrf_token %}
|
||||
{{ form.content }}
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
var container = document.getElementById("post-list");
|
||||
let autoScroll = true
|
||||
function isAtBottom(){
|
||||
return container.scrollHeight - container.scrollTop - container.clientHeight < 50;
|
||||
}
|
||||
|
||||
container.addEventListener("scroll", () => {
|
||||
autoScroll = isAtBottom();
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:afterSwap", (e) => {
|
||||
if(e.target.id !== "post-list") return
|
||||
if(autoScroll){ container.scrollTop = container.scrollHeight; }
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 5.2.9 on 2026-03-05 12:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0003_userdata_delete_profile'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserAction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=200)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -11,13 +11,20 @@ class UserData(models.Model):
|
|||
equipment = models.CharField(max_length=1024, default='0;')
|
||||
inventory = models.CharField(max_length=1024, default='0;')
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}'s data"
|
||||
|
||||
|
||||
# So this is my beutiful brainchild to keep user data in about 1KB per user, I'm not too sure but still
|
||||
# it's worth the try, so the main payload is the inventory along the soliers an their weapons and vehicles
|
||||
# along with the mods for everything which I have separated by `;`, `.` and `,` for soldier, equipment
|
||||
# class and each equipment. I'll try a bit of bit-hacking to pack as much info of 20 bytes in 2 of the mods.
|
||||
# ammo|--|sights|---|internals|---|, trigger|--|barrel|--|skins|----|, number|--------|
|
||||
# So this is my beautiful brainchild to keep user data in about 1KB per user, I'm not too sure but still
|
||||
# it's worth the try, so the main payload is the inventory along the soldiers and their weapons and
|
||||
# vehicles along with the mods for everything which I have separated by `;`, `.` and `,` for soldier,
|
||||
# equipment class and each equipment. I'll try a bit of bit-hacking to pack info of as much as 20 bytes
|
||||
# in 2 for the mods. ammo|--|sights|---|internals|---|, trigger|--|barrel|--|skins|----|, number|--------|
|
||||
|
||||
|
||||
class UserAction(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
action = models.CharField(max_length=200)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}'s action"
|
||||
|
|
|
|||
|
|
@ -5,3 +5,9 @@ class UserDataSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = UserData
|
||||
fields = ['user', 'name', 'xp', 'money', 'equipment', 'inventory']
|
||||
|
||||
|
||||
class UserActionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = UserAction
|
||||
fields = ['user', 'action']
|
||||
|
|
|
|||
Loading…
Reference in New Issue