local_settings.py
db.sqlite3
db.sqlite3-journal
+staticfiles/
# Flask stuff:
instance/
--- /dev/null
+How to deploy on Debian 11
+==========================
+
+
+Dependencies:
+==========================
+Python
+--------------------------
+python3
+
+Django
+--------------------------
+python3-django
+python3-django-allauth
+python3-psycopg2
+python3-whitenoise
+libjs-jquery
+
+Serving
+--------------------------
+gunicorn
+nginx
+
+SSL
+--------------------------
+certbot
+python3-certbot-nginx
+
+Postgres
+--------------------------
+postgresql
+postgresql-contrib
+
+
+Installation Instructions:
+==========================
+ 1. Install the necessary dependencies
+ 2. Create the directories; change example.com to your domain
+ a. /var/log/gunicorn/
+ b. /var/run/gunicorn/
+ c. /var/www/example.com/
+ d. /usr/local/etc/gunicorn/
+ 3. Copy example.com.service to /etc/systemd/system/ and change the name to your domain
+ 4. Edit the values of example.com.service by changing example.com to your domain
+ 5. Copy example.com.env to /usr/local/etc/gunicorn/ and change the name to your domain
+ 6. Edit the values of example.com.env
+ a. Generate a secret key by running generate_random_key.sh
+ 7. Change the user and group ownership of (2.abd) directories to www-data recursively
+ 8. In postgres create a user with a password and a database owned by the user
+ 9. Export your environment variables and inside core, run `python3 manage.py migrate`
+10. Inside core, run `python3 manage.py createsuperuser`
+11. Inside core, run `python3 manage.py collectstatic`
+12. Copy the contents of core/ into /var/www/example.com/
+13. Copy example.com.nginx to /etc/nginx/sites-available/ and change the name
+14. Edit the file example.com.nginx replacing example.com with your domain
+15. Run certbot to verify your domain
+16. Enable gunicorn `systemctl enable --now example.com.service`
+17. Enable nginx `systemctl enable --now example.com.service`
+
+
+Finalising Deployment:
+==========================
+1. Log into the admin panel of your website and edit / create site values in "Sites"
+2. Create a Social Application.
+3. Create a ResetData object.
- Display clip info more clearly
-- Clip comments?
+
+- Allow users to delete clips they've added during a block
+
+- Add left and right buttons on clips during ranking
+
+- Show a list of all clips during ranking with a button to quickly go to
+
+- A button to quickly go to the specific clip ranking from the results
+
+- Let users view clips uploaded by others and let them see the final page
+
+- Show historical rankings
+
+- If a clip has been ranked, show its ranking during the ranking phase
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = "django-insecure-!nzce=4gwub(n+!6@lh^vjl+m(*ibo42&gav2kvr@d4!$3ausi"
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
+DEBUG = os.getenv("CLIP_RANKER_DEBUG", "") != "False"
if DEBUG:
import dotenv
dotenv.load_dotenv()
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = os.getenv("CLIP_RANKER_ALLOWED_HOSTS", "").split(";")
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.getenv(
+ "CLIP_RANKER_SECRET_KEY", "04t8gx%o4!^1ae_@%3r@vb^=lx^r$%u*5&+88568$s%w)u1b@f"
+)
# Application definition
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "whitenoise.middleware.WhiteNoiseMiddleware",
]
ROOT_URLCONF = "core.urls"
DATABASES = {
"default": {
- "ENGINE": "django.db.backends.sqlite3",
- "NAME": BASE_DIR / "db.sqlite3",
+ "ENGINE": "django.db.backends.postgresql",
+ "NAME": os.getenv("CLIP_RANKER_DB_NAME"),
+ "USER": os.getenv("CLIP_RANKER_DB_USER"),
+ "PASSWORD": os.getenv("CLIP_RANKER_DB_PASS"),
+ "HOST": os.getenv("CLIP_RANKER_DB_HOST"),
+ "PORT": os.getenv("CLIP_RANKER_DB_PORT"),
}
}
-
# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/
-STATIC_URL = "static/"
+STATIC_URL = "/static/"
+
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
+STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
+
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
-# Generated by Django 4.1.5 on 2023-01-22 12:47
+# Generated by Django 4.1.5 on 2023-01-25 14:32
+import datetime
+from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
initial = True
dependencies = [
- ("socialaccount", "0003_extra_data_default_dict"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
+ migrations.CreateModel(
+ name="ResetData",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("date_time", models.DateTimeField()),
+ (
+ "end_date_time",
+ models.DateTimeField(default=datetime.datetime(2038, 1, 19, 3, 14)),
+ ),
+ ("max_clips", models.IntegerField(default=2)),
+ ("ranks", models.IntegerField(default=5)),
+ ("user_created_clip", models.BooleanField(default=True)),
+ (
+ "clip_newer_than",
+ models.DateTimeField(
+ default=datetime.datetime(1970, 1, 1, 0, 0, 1)
+ ),
+ ),
+ ],
+ ),
migrations.CreateModel(
name="Clip",
fields=[
(
"id",
- models.CharField(max_length=100, primary_key=True, serialize=False),
+ models.CharField(
+ max_length=100, primary_key=True, serialize=False, unique=True
+ ),
),
("url", models.URLField(null=True)),
("embed_url", models.URLField(null=True)),
("thumbnail_url", models.URLField(null=True)),
("duration", models.FloatField(null=True)),
("vod_offset", models.IntegerField(null=True)),
+ ("date_added", models.DateTimeField(null=True)),
+ ("rank", models.IntegerField(null=True)),
+ (
+ "account",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="AllowedChannel",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("broadcaster_id", models.CharField(max_length=100)),
(
- "user",
+ "reset_data",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
- to="socialaccount.socialaccount",
+ to="manager.resetdata",
),
),
],
+ options={
+ "unique_together": {("broadcaster_id", "reset_data")},
+ },
),
]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 12:48
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0001_initial"),
- ]
-
- operations = [
- migrations.RenameField(
- model_name="clip",
- old_name="user",
- new_name="account",
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 12:52
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ("manager", "0002_rename_user_clip_account"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="clip",
- name="account",
- field=models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
- ),
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 13:10
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0003_alter_clip_account"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="clip",
- name="id",
- field=models.CharField(
- max_length=100, primary_key=True, serialize=False, unique=True
- ),
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 13:30
-
-from django.conf import settings
-import django.contrib.auth.models
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("auth", "0012_alter_user_first_name_max_length"),
- ("manager", "0004_alter_clip_id"),
- ]
-
- operations = [
- migrations.CreateModel(
- name="MyUser",
- fields=[
- (
- "user_ptr",
- models.OneToOneField(
- auto_created=True,
- on_delete=django.db.models.deletion.CASCADE,
- parent_link=True,
- primary_key=True,
- serialize=False,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ("clip_amount", models.IntegerField(default=0)),
- ],
- options={
- "verbose_name": "user",
- "verbose_name_plural": "users",
- "abstract": False,
- },
- bases=("auth.user",),
- managers=[
- ("objects", django.contrib.auth.models.UserManager()),
- ],
- ),
- migrations.AlterField(
- model_name="clip",
- name="account",
- field=models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE, to="manager.myuser"
- ),
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 13:42
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ("manager", "0005_myuser_alter_clip_account"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="clip",
- name="date_added",
- field=models.DateTimeField(null=True),
- ),
- migrations.AlterField(
- model_name="clip",
- name="account",
- field=models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
- ),
- ),
- migrations.DeleteModel(
- name="MyUser",
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 13:50
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0006_clip_date_added_alter_clip_account_delete_myuser"),
- ]
-
- operations = [
- migrations.CreateModel(
- name="ResetTime",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("date_time", models.DateTimeField()),
- ],
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 13:58
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0007_resettime"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="resettime",
- name="clip_amount",
- field=models.IntegerField(default=2),
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 14:00
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0008_resettime_clip_amount"),
- ]
-
- operations = [
- migrations.RenameField(
- model_name="resettime",
- old_name="clip_amount",
- new_name="max_clips",
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 14:02
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0009_rename_clip_amount_resettime_max_clips"),
- ]
-
- operations = [
- migrations.RenameModel(
- old_name="ResetTime",
- new_name="ResetData",
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-22 16:24
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0010_rename_resettime_resetdata"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="clip",
- name="rank",
- field=models.IntegerField(null=True),
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-23 00:22
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0011_clip_rank"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="resetdata",
- name="ranks",
- field=models.IntegerField(default=5),
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-23 11:51
-
-import datetime
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0012_resetdata_ranks"),
- ]
-
- operations = [
- migrations.AddField(
- model_name="resetdata",
- name="clip_limit_to_channels",
- field=models.JSONField(null=True),
- ),
- migrations.AddField(
- model_name="resetdata",
- name="clip_newer_than",
- field=models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0, 1)),
- ),
- migrations.AddField(
- model_name="resetdata",
- name="user_created_clip",
- field=models.BooleanField(default=True),
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-23 12:03
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0013_resetdata_clip_limit_to_channels_and_more"),
- ]
-
- operations = [
- migrations.CreateModel(
- name="AllowedChannel",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("broadcaster_id", models.CharField(max_length=100)),
- (
- "reset_data",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="manager.resetdata",
- ),
- ),
- ],
- ),
- ]
+++ /dev/null
-# Generated by Django 4.1.5 on 2023-01-23 12:14
-
-import datetime
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ("manager", "0014_allowedchannel"),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name="resetdata",
- name="clip_limit_to_channels",
- ),
- migrations.AddField(
- model_name="resetdata",
- name="end_date_time",
- field=models.DateTimeField(default=datetime.datetime(2038, 1, 19, 3, 14)),
- ),
- ]
if not allowed_channels.exists():
raise ChannelNotAllowedError
- if (
- reset_data.clip_newer_than
- > datetime.datetime.strptime(data["created_at"], "%Y-%m-%dT%H:%M:%S%z")
+ if reset_data.clip_newer_than > datetime.datetime.strptime(
+ data["created_at"], "%Y-%m-%dT%H:%M:%S%z"
):
raise ClipTooOldError
<div id="group">
<h1>uploaded by: {{ video.account.username }}</h1>
<iframe
- src="https://clips.twitch.tv/embed?clip={{ video.id }}&parent=localhost"
+ src="https://clips.twitch.tv/embed?clip={{ video.id }}{% for site in sites %}&parent={{ site.domain }}{% endfor %}"
height="600"
width="1000"
preload="none"
{% if video.rank == i %}
<td>
<iframe
- src="https://clips.twitch.tv/embed?clip={{ video.id }}&parent=localhost"
+ src="https://clips.twitch.tv/embed?clip={{ video.id }}{% for site in sites %}&parent={{ site.domain }}{% endfor %}"
height="135"
width="240"
preload="none"
from django.shortcuts import render, redirect
from django.core.exceptions import ValidationError
from django.contrib.admin.views.decorators import staff_member_required
+from django.contrib.sites.models import Site
from .request_clip import request_clip
from .forms import ClipForm, RankForm
NotFoundError,
)
from .models import Clip, ResetData
-import traceback
@staff_member_required
reset_data = ResetData.objects.latest("date_time")
reset_time = reset_data.date_time
try:
- video = Clip.objects.filter(
- date_added__gt=reset_time
- ).order_by("date_added")[id - 1]
+ video = Clip.objects.filter(date_added__gt=reset_time).order_by("date_added")[
+ id - 1
+ ]
except IndexError:
return redirect(final_ranking)
if request.method == "POST":
context={
"video": video,
"ranks": range(reset_data.ranks, 0, -1),
+ "sites": Site.objects.all(),
},
)
context={
"videos": Clip.objects.filter(date_added__gt=reset_time),
"ranks": range(reset_data.ranks, 0, -1),
+ "sites": Site.objects.all(),
},
)
--- /dev/null
+CLIP_RANKER_DEBUG="False"
+CLIP_RANKER_ALLOWED_HOSTS="example.com;www.example.com"
+CLIP_RANKER_SECRET_KEY="yoursecretkey"
+CLIP_RANKER_STATIC_ROOT="/var/www/example.com"
+CLIP_RANKER_DB_USER="username"
+CLIP_RANKER_DB_PASS="password"
+CLIP_RANKER_DB_HOST="localhost"
+CLIP_RANKER_DB_PORT="5432"
+CLIP_RANKER_DB_NAME="dbname"
--- /dev/null
+server {
+ server_name example.com www.example.com;
+
+ location / {
+ include proxy_params;
+ proxy_pass http://unix:/var/run/gunicorn/example.com.sock;
+ }
+
+ location = /favicon.ico {
+ access_log off;
+ log_not_found off;
+ }
+
+ location /static/ {
+ alias /var/www/example.com/staticfiles/;
+ }
+}
--- /dev/null
+[Unit]
+Description=example.com gunicorn daemon
+After=network.target
+
+[Service]
+User=www-data
+Group=www-data
+WorkingDirectory=/var/www/example.com/
+EnvironmentFile=/usr/local/etc/gunicorn/example.com.env
+StandardOutput=file:/var/log/gunicorn/example.com.log
+StandardError=file:/var/log/gunicorn/example.com.log
+ExecStart=gunicorn3 --access-logfile - --workers 3 --bind unix:/var/run/gunicorn/example.com.sock core.wsgi:application
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null
+#!/bin/sh
+
+python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
--- /dev/null
+asgiref==3.6.0
+certifi==2022.12.7
+cffi==1.15.1
+charset-normalizer==3.0.1
+cryptography==39.0.0
+defusedxml==0.7.1
+dj-database-url==1.2.0
+Django==4.1.5
+django-allauth==0.52.0
+gunicorn==20.1.0
+idna==3.4
+oauthlib==3.2.2
+psycopg2==2.9.5
+pycparser==2.21
+PyJWT==2.6.0
+python-dotenv==0.21.0
+python3-openid==3.2.0
+pytz==2022.7.1
+requests==2.28.2
+requests-oauthlib==1.3.1
+sqlparse==0.4.3
+urllib3==1.26.14
+whitenoise==6.3.0