Added UserAction to API and Chat

with_posts_new
Surya_AI 2026-03-08 17:00:20 +05:30
parent d774f2f93c
commit 5b85fabcbd
18 changed files with 355 additions and 20 deletions

3
.gitignore vendored
View File

@ -166,3 +166,6 @@ staticfiles/
# database
db.sqlite3
# todo
todo*

15
api/actions.py Normal file
View File

@ -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)

19
api/data.py Normal file
View File

@ -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

View File

@ -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)

View File

@ -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()),
]

View File

@ -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)

9
blog/forms.py Normal file
View File

@ -0,0 +1,9 @@
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['content']

View File

@ -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)

View File

@ -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'),
]

View File

@ -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'})

Binary file not shown.

View File

@ -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">

View File

@ -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>

View File

@ -0,0 +1,3 @@
{% for post in posts reversed %}
{% include "blog/partials/post.html" %}
{% endfor %}

View File

@ -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 %}

View File

@ -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)),
],
),
]

View File

@ -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"

View File

@ -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']