Add initial project setup with Django configuration
Introduce basic Django project structure, including ASGI application settings, forms setup for Playthrough model, command utilities, and initial migration files. Includes proper `.gitignore` definitions for Python and Django development standards. Initialize Bootstrap with custom theming and necessary static assets for frontend styling. Dedicate management command for setting outbound redirect destinations, enhancing administrative capabilities.
This commit is contained in:
commit
e4ae60d336
23 changed files with 601 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
db.sqlite3
|
||||
*.pyc
|
||||
__pycache__/
|
||||
venv/
|
0
exp360test/__init__.py
Normal file
0
exp360test/__init__.py
Normal file
16
exp360test/asgi.py
Normal file
16
exp360test/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for exp360test 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', 'exp360test.settings')
|
||||
|
||||
application = get_asgi_application()
|
9
exp360test/forms.py
Normal file
9
exp360test/forms.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.forms import ModelForm
|
||||
|
||||
from .models import Playthrough
|
||||
|
||||
|
||||
class PlaythroughForm(ModelForm):
|
||||
class Meta:
|
||||
model = Playthrough
|
||||
fields = ["email"]
|
25
exp360test/management/commands/setdestination.py
Normal file
25
exp360test/management/commands/setdestination.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from dbsettings.models import Setting
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Set the destination of the outward redirect"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("destination", type=str)
|
||||
parser.add_argument("password", type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
Setting.objects.update_or_create(
|
||||
key="destination",
|
||||
defaults={"value": options["destination"]},
|
||||
)
|
||||
|
||||
Setting.objects.update_or_create(
|
||||
key="password",
|
||||
defaults={"value": options["password"]},
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Successfully set the destination to {options['destination']}")
|
||||
)
|
23
exp360test/migrations/0001_initial.py
Normal file
23
exp360test/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.0.1 on 2024-02-02 19:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Playthrough',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('start', models.DateTimeField(auto_now_add=True)),
|
||||
('end', models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
19
exp360test/migrations/0002_playthrough_session.py
Normal file
19
exp360test/migrations/0002_playthrough_session.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 5.0.1 on 2024-02-02 19:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('exp360test', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='playthrough',
|
||||
name='session',
|
||||
field=models.CharField(default=None, max_length=100, unique=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
18
exp360test/migrations/0003_alter_playthrough_session.py
Normal file
18
exp360test/migrations/0003_alter_playthrough_session.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.0.1 on 2024-02-02 20:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('exp360test', '0002_playthrough_session'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='playthrough',
|
||||
name='session',
|
||||
field=models.UUIDField(unique=True),
|
||||
),
|
||||
]
|
0
exp360test/migrations/__init__.py
Normal file
0
exp360test/migrations/__init__.py
Normal file
24
exp360test/models.py
Normal file
24
exp360test/models.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class PlaythroughManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.annotate(
|
||||
duration=models.ExpressionWrapper(
|
||||
models.F("end") - models.F("start"),
|
||||
output_field=models.DurationField(),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Playthrough(models.Model):
|
||||
session = models.UUIDField(unique=True)
|
||||
email = models.EmailField(unique=True)
|
||||
start = models.DateTimeField(auto_now_add=True)
|
||||
end = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
objects = PlaythroughManager()
|
129
exp360test/settings.py
Normal file
129
exp360test/settings.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
"""
|
||||
Django settings for exp360test project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.0.1.
|
||||
|
||||
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
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-bh1y%s73i)w(h=0l+h)%8nuj8pxe$5hatd#gu2bn%q@2y0#t^w'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# Hosts for CSRF
|
||||
CSRF_TRUSTED_ORIGINS = ["https://exp360test.kumi.live", "https://exp360test.dev.kumi"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'exp360test',
|
||||
'dbsettings',
|
||||
]
|
||||
|
||||
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 = 'exp360test.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 = 'exp360test.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'
|
6
exp360test/static/dist/css/bootstrap.min.css
vendored
Normal file
6
exp360test/static/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
exp360test/static/dist/css/bootstrap.min.css.map
vendored
Normal file
1
exp360test/static/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
32
exp360test/templates/base.html
Normal file
32
exp360test/templates/base.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>EXP360 Test</title>
|
||||
<link rel="stylesheet" href="{% static 'dist/css/bootstrap.min.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-light bg-light">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">EXP360 Test</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main style="margin-top: 30px;" role="main" class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="text-muted py-5">
|
||||
<div class="container">
|
||||
<p style="visibility: hidden;" class="float-right">
|
||||
<a href="#">Back to top</a>
|
||||
</p>
|
||||
<p>Kumi Systems e.U. © 2024</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
36
exp360test/templates/end.html
Normal file
36
exp360test/templates/end.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>End</h1>
|
||||
<p>Thank you for participating. You took {{ duration.total_seconds }} seconds to complete the tour.</p>
|
||||
<p>You may now close this window.</p>
|
||||
|
||||
<h2>Leaderboard</h2>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Email</th>
|
||||
<th>Duration</th>
|
||||
</tr>
|
||||
{% for entry in leaderboard %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ entry.email }}</td>
|
||||
<td>{{ entry.duration }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<form action="/restart" method="get">
|
||||
<input class="btn btn-secondary" type="submit" value="Restart">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if redirect %}
|
||||
<script>
|
||||
window.history.replaceState({}, "", null);
|
||||
window.location.replace("{{ redirect }}");
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
21
exp360test/templates/leaderboard.html
Normal file
21
exp360test/templates/leaderboard.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>Leaderboard</h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Email</th>
|
||||
<th>Duration</th>
|
||||
</tr>
|
||||
{% for entry in leaderboard %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ entry.email }}</td>
|
||||
<td>{{ entry.duration }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
16
exp360test/templates/redirect_out.html
Normal file
16
exp360test/templates/redirect_out.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Redirect</h1>
|
||||
<p>Please wait while we redirect you to the content, or click the button below.</p>
|
||||
<form action="{{ destination }}" method="post">
|
||||
<input type="hidden" name="password" value="{{ password }}">
|
||||
<input type="submit" value="Redirect">
|
||||
</form>
|
||||
<script>
|
||||
window.history.replaceState({}, "", null);
|
||||
document.querySelector('form').submit();
|
||||
</script>
|
||||
</html>
|
16
exp360test/templates/start.html
Normal file
16
exp360test/templates/start.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>Start</h1>
|
||||
<p>Please enter your email address to continue.</p>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<input class="btn btn-primary" type="submit" value="Start">
|
||||
</form>
|
||||
<hr style="margin-top: 10px;">
|
||||
<form action="/leaderboard" method="get">
|
||||
<input class="btn btn-secondary" type="submit" value="Leaderboard">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
38
exp360test/urls.py
Normal file
38
exp360test/urls.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
"""
|
||||
URL configuration for exp360test 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
|
||||
|
||||
from .views import (
|
||||
StartView,
|
||||
EndView,
|
||||
RedirectInView,
|
||||
RedirectOutView,
|
||||
RestartView,
|
||||
LeaderboardView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", StartView.as_view(), name="start"),
|
||||
path("end/", EndView.as_view(), name="end"),
|
||||
path("redirect_in/", RedirectInView.as_view(), name="redirect_in"),
|
||||
path("redirect_out/", RedirectOutView.as_view(), name="redirect_out"),
|
||||
path("restart/", RestartView.as_view(), name="restart"),
|
||||
path("leaderboard/", LeaderboardView.as_view(), name="leaderboard"),
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
128
exp360test/views.py
Normal file
128
exp360test/views.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
from django.views.generic import TemplateView, View, FormView
|
||||
from django.shortcuts import resolve_url, redirect
|
||||
from django.db.models import F, ExpressionWrapper, fields
|
||||
from django.contrib import messages
|
||||
from django.forms import ValidationError
|
||||
from django.http import Http404
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import uuid
|
||||
|
||||
from .models import Playthrough
|
||||
from .forms import PlaythroughForm
|
||||
|
||||
from dbsettings.models import Setting
|
||||
|
||||
|
||||
class StartView(FormView):
|
||||
template_name = "start.html"
|
||||
form_class = PlaythroughForm
|
||||
|
||||
def form_valid(self, form):
|
||||
session_id = self.request.session.get("session_id")
|
||||
|
||||
if session_id is None:
|
||||
session_id = str(uuid.uuid4())
|
||||
self.request.session["session_id"] = session_id
|
||||
|
||||
form.instance.session = session_id
|
||||
form.save()
|
||||
|
||||
return redirect("redirect_out")
|
||||
|
||||
def form_invalid(self, form):
|
||||
for field, errors in form.errors.items():
|
||||
for error in errors:
|
||||
messages.error(self.request, f"{field}: {error}")
|
||||
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
session_id = request.session.get("session_id")
|
||||
|
||||
if session_id is not None:
|
||||
try:
|
||||
playthrough = Playthrough.objects.get(session=session_id)
|
||||
except Playthrough.DoesNotExist:
|
||||
request.session["session_id"] = None
|
||||
else:
|
||||
if playthrough.end is None:
|
||||
return redirect("redirect_out")
|
||||
else:
|
||||
return redirect("end")
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class EndView(TemplateView):
|
||||
template_name = "end.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
try:
|
||||
playthrough = Playthrough.objects.get(
|
||||
session=self.request.session.get("session_id")
|
||||
)
|
||||
except Playthrough.DoesNotExist:
|
||||
context["redirect"] = resolve_url("start")
|
||||
return context
|
||||
|
||||
if playthrough.end is None:
|
||||
context["redirect"] = resolve_url("redirect_out")
|
||||
return context
|
||||
|
||||
context["duration"] = playthrough.duration
|
||||
context["leaderboard"] = Playthrough.objects.filter(
|
||||
duration__isnull=False
|
||||
).order_by("duration")[:10]
|
||||
return context
|
||||
|
||||
|
||||
class RedirectOutView(TemplateView):
|
||||
template_name = "redirect_out.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["destination"] = Setting.objects.get(key="destination").value
|
||||
context["password"] = Setting.objects.get(key="password").value
|
||||
return context
|
||||
|
||||
|
||||
class RedirectInView(View):
|
||||
template_name = "redirect_in.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
session_id = request.session.get("session_id")
|
||||
|
||||
if session_id is None:
|
||||
return redirect("start")
|
||||
|
||||
try:
|
||||
playthrough = Playthrough.objects.get(session=session_id)
|
||||
except Playthrough.DoesNotExist:
|
||||
return redirect("start")
|
||||
|
||||
if playthrough.end is None:
|
||||
playthrough.end = datetime.now()
|
||||
playthrough.save()
|
||||
|
||||
return redirect("end")
|
||||
|
||||
|
||||
class RestartView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
request.session["session_id"] = None
|
||||
return redirect("start")
|
||||
|
||||
|
||||
class LeaderboardView(TemplateView):
|
||||
template_name = "leaderboard.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["leaderboard"] = Playthrough.objects.filter(
|
||||
duration__isnull=False
|
||||
).order_by("duration")
|
||||
return context
|
16
exp360test/wsgi.py
Normal file
16
exp360test/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for exp360test 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', 'exp360test.settings')
|
||||
|
||||
application = get_wsgi_application()
|
22
manage.py
Executable file
22
manage.py
Executable 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', 'exp360test.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()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Django
|
||||
dbsettings
|
Loading…
Reference in a new issue