feat: add Django project & VSCode config for enhanced development workflow

Introduce the initial Django project setup alongside necessary configurations for VSCode, laying the groundwork for a robust development environment. This includes project scaffolding (`coffeemachine` app), ASGI configuration for async support, and a model example for game servers, demonstrating Django's ORM capabilities. Additionally, we integrate Bootstrap for frontend styling and configure `.gitignore` for Python/Django standard exclusions, ensuring a cleaner repository state. The VSCode launch configuration is tailored for Django, facilitating debugging and enhancing the developer experience within the IDE.

This structured approach not only accelerates the setup phase for new developers but also ensures consistency across environments, fostering a more collaborative and efficient development process.
This commit is contained in:
Kumi 2024-06-05 15:35:47 +02:00
commit 9ff6278ba1
Signed by: kumi
GPG key ID: ECBCC9082395383F
24 changed files with 541 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
venv/
*.pyc
__pycache__/
settings.ini
db.sqlite3

20
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Django",
"type": "debugpy",
"request": "launch",
"args": [
"runserver",
"8104"
],
"django": true,
"autoStartBrowser": false,
"program": "${workspaceFolder}/manage.py"
}
]
}

View file

16
coffeemachine/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for coffeemachine project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'coffeemachine.settings')
application = get_asgi_application()

View file

View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ServersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'coffeemachine.servers'

View file

@ -0,0 +1,19 @@
from django.db import models
class GameServer(models.Model):
STATUS_CHOICES = [
('stopped', 'Stopped'),
('running', 'Running'),
('installing', 'Installing'),
('updating', 'Updating'),
('error', 'Error'),
]
name = models.CharField(max_length=100)
game = models.CharField(max_length=100)
status = models.CharField(max_length=100, choices=STATUS_CHOICES, default='stopped')
installation_path = models.CharField(max_length=255)
custom_script_content = models.TextField(blank=True, null=True)
def __str__(self):
return self.name

View file

@ -0,0 +1,36 @@
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}CoffeeMachine{% endblock %}</title>
<link href="{% static "dist/css/bootstrap.min.css" %}" rel="stylesheet">
<script src="{% static "dist/js/bootstrap.bundle.min.js" %}"></script>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'dashboard' %}">CoffeeMachine</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{% url 'dashboard' %}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'install_server' %}">Install Server</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View file

@ -0,0 +1,32 @@
{% extends "servers/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>Game Server Dashboard</h1>
<a href="{% url 'install_server' %}" class="btn btn-primary mb-3">Install New Server</a>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Game</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr>
<td>{{ server.name }}</td>
<td>{{ server.game }}</td>
<td>{{ server.get_status_display }}</td>
<td>
<a href="{% url 'manage_server' server.id 'start' %}" class="btn btn-success btn-sm">Start</a>
<a href="{% url 'manage_server' server.id 'stop' %}" class="btn btn-danger btn-sm">Stop</a>
<a href="{% url 'manage_server' server.id 'update' %}" class="btn btn-warning btn-sm">Update</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends "base.html" %} {% block title %}Install Game Server{% endblock %} {%
block content %}
<h1>Install Game Server</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">{{ form.name.label_tag }} {{ form.name }}</div>
<div class="mb-3">{{ form.game.label_tag }} {{ form.game }}</div>
<div class="mb-3">
{{ form.installation_path.label_tag }} {{ form.installation_path }}
</div>
<div class="mb-3 form-check">
{{ form.existing_server }} {{ form.existing_server.label_tag }}
</div>
<div class="mb-3">
{{ form.script_file.label_tag }} {{ form.script_file }}
</div>
<div class="mb-3">
{{ form.script_content.label_tag }} {{ form.script_content }}
</div>
<button type="submit" class="btn btn-primary">Install</button>
</form>
{% endblock %}

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,12 @@
from django.urls import path
from .views import DashboardView, InstallServerView, ManageServerView
urlpatterns = [
path("", DashboardView.as_view(), name="dashboard"),
path("install/", InstallServerView.as_view(), name="install_server"),
path(
"manage/<int:server_id>/<str:action>/",
ManageServerView.as_view(),
name="manage_server",
),
]

View file

@ -0,0 +1,161 @@
# servers/views.py
from django.shortcuts import render, redirect
from django.views import View
from .models import GameServer
from .forms import GameServerForm
import subprocess
import os
import urllib.request
class DashboardView(View):
template_name = "servers/dashboard.html"
def get(self, request, *args, **kwargs):
servers = GameServer.objects.all()
for server in servers:
server.status = self.check_server_status(server)
server.save()
return render(request, self.template_name, {"servers": servers})
def check_server_status(self, server):
script_path = (
os.path.join(server.installation_path, "custom_script.sh")
if server.custom_script_content
else f"./{server.game}server"
)
result = subprocess.run(
[script_path, "status"],
capture_output=True,
text=True,
cwd=server.installation_path,
)
if "is running" in result.stdout:
return "running"
else:
return "stopped"
class InstallServerView(View):
template_name = "servers/install_server.html"
form_class = GameServerForm
def get(self, request, *args, **kwargs):
form = self.form_class()
return render(request, self.template_name, {"form": form})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST, request.FILES)
if form.is_valid():
server = form.save(commit=False)
os.makedirs(server.installation_path, exist_ok=True)
if form.cleaned_data["existing_server"]:
# Handle existing server
script_path = self.handle_custom_script(form, server)
server.status = self.check_server_status(server)
else:
# Handle new server installation
script_path = self.handle_custom_script(form, server)
if not script_path:
script_path = self.download_lgsm_script(server.installation_path)
server.status = "installing"
server.save()
subprocess.run(
[script_path, server.game], cwd=server.installation_path
)
subprocess.run(
[f"./{server.game}server", "install"],
cwd=server.installation_path,
)
else:
server.status = "installing"
server.save()
subprocess.run(
[script_path, "install"], cwd=server.installation_path
)
server.status = self.check_server_status(server)
server.save()
return redirect("dashboard")
return render(request, self.template_name, {"form": form})
def handle_custom_script(self, form, server):
script_path = None
if form.cleaned_data["script_file"]:
script_file = form.cleaned_data["script_file"]
script_path = os.path.join(server.installation_path, "custom_script.sh")
with open(script_path, "wb+") as destination:
for chunk in script_file.chunks():
destination.write(chunk)
os.chmod(script_path, 0o755) # Make the script executable
elif form.cleaned_data["script_content"]:
script_content = form.cleaned_data["script_content"]
script_path = os.path.join(server.installation_path, "custom_script.sh")
with open(script_path, "w") as script_file:
script_file.write(script_content)
os.chmod(script_path, 0o755) # Make the script executable
server.custom_script_content = script_content
return script_path
def download_lgsm_script(self, installation_path):
url = "https://linuxgsm.sh"
script_path = os.path.join(installation_path, "linuxgsm.sh")
urllib.request.urlretrieve(url, script_path)
os.chmod(script_path, 0o755) # Make the script executable
return script_path
def check_server_status(self, server):
script_path = (
os.path.join(server.installation_path, "custom_script.sh")
if server.custom_script_content
else f"./{server.game}server"
)
result = subprocess.run(
[script_path, "status"],
capture_output=True,
text=True,
cwd=server.installation_path,
)
if "is running" in result.stdout:
return "running"
else:
return "stopped"
class ManageServerView(View):
def post(self, request, server_id, action, *args, **kwargs):
server = GameServer.objects.get(id=server_id)
script_path = (
os.path.join(server.installation_path, "custom_script.sh")
if server.custom_script_content
else f"./{server.game}server"
)
command = [script_path, action]
server.status = (
"updating"
if action == "update"
else "running" if action == "start" else "stopped"
)
server.save()
subprocess.run(command, cwd=server.installation_path)
server.status = self.check_server_status(server)
server.save()
return redirect("dashboard")
def check_server_status(self, server):
script_path = (
os.path.join(server.installation_path, "custom_script.sh")
if server.custom_script_content
else f"./{server.game}server"
)
result = subprocess.run(
[script_path, "status"],
capture_output=True,
text=True,
cwd=server.installation_path,
)
if "is running" in result.stdout:
return "running"
else:
return "stopped"

125
coffeemachine/settings.py Normal file
View file

@ -0,0 +1,125 @@
"""
Django settings for coffeemachine project.
Generated by 'django-admin startproject' using Django 5.0.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
from autosecretkey import AutoSecretKey
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
ASK = AutoSecretKey(BASE_DIR / 'settings.ini')
SECRET_KEY = ASK.secret_key
CONFIG = ASK.config
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.getboolean("CoffeeMachine", "Debug", fallback=False)
ALLOWED_HOSTS = CONFIG.get("CoffeeMachine", "Hostname", fallback="*").split(",")
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'coffeemachine.servers',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'coffeemachine.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'coffeemachine.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

23
coffeemachine/urls.py Normal file
View file

@ -0,0 +1,23 @@
"""
URL configuration for coffeemachine project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
]

16
coffeemachine/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for coffeemachine project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'coffeemachine.settings')
application = get_wsgi_application()

22
manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'coffeemachine.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
Django
psycopg2-binary
django-autosecretkey
celery
redis