From 27708152d2b6a4cb478d481342c68ffe74cd6bb8 Mon Sep 17 00:00:00 2001
From: Nick Sweeting <git@nicksweeting.com>
Date: Tue, 2 Apr 2019 16:36:41 -0400
Subject: [PATCH] wip initial django setup

---
 archivebox/__init__.py                        |   4 +-
 archivebox/archivebox/VERSION                 |   1 +
 archivebox/archivebox/__init__.py             |   0
 archivebox/archivebox/settings.py             | 123 ++++++++++++++++++
 archivebox/archivebox/urls.py                 |  21 +++
 archivebox/archivebox/wsgi.py                 |  16 +++
 archivebox/core/__init__.py                   |   0
 archivebox/core/admin.py                      |   3 +
 archivebox/core/apps.py                       |   5 +
 archivebox/{ => core}/archive.py              |  16 +--
 archivebox/{ => core}/archive_methods.py      |  10 +-
 archivebox/{ => core}/config.py               |   0
 archivebox/{ => core}/index.py                |  14 +-
 archivebox/{ => core}/links.py                |   6 +-
 archivebox/{ => core}/logs.py                 |   4 +-
 .../core/management/commands/archivebox.py    |  10 ++
 archivebox/core/migrations/__init__.py        |   0
 archivebox/core/models.py                     |   3 +
 archivebox/{ => core}/parse.py                |   4 +-
 archivebox/{ => core}/purge.py                |   4 +-
 archivebox/{ => core}/schema.py               |  28 ++--
 archivebox/core/tests.py                      |   3 +
 archivebox/{ => core}/util.py                 |  22 ++--
 archivebox/core/views.py                      |   3 +
 archivebox/manage.py                          |  15 +++
 archivebox/{templates => }/static/archive.png | Bin
 .../{templates => }/static/bootstrap.min.css  |   0
 .../{templates => }/static/external.png       | Bin
 .../static/jquery.dataTables.min.css          |   0
 .../static/jquery.dataTables.min.js           |   0
 .../{templates => }/static/jquery.min.js      |   0
 .../{templates => }/static/sort_asc.png       | Bin
 .../{templates => }/static/sort_both.png      | Bin
 .../{templates => }/static/sort_desc.png      | Bin
 archivebox/{templates => }/static/spinner.gif | Bin
 requirements.txt                              |   1 +
 36 files changed, 257 insertions(+), 59 deletions(-)
 create mode 120000 archivebox/archivebox/VERSION
 create mode 100644 archivebox/archivebox/__init__.py
 create mode 100644 archivebox/archivebox/settings.py
 create mode 100644 archivebox/archivebox/urls.py
 create mode 100644 archivebox/archivebox/wsgi.py
 create mode 100644 archivebox/core/__init__.py
 create mode 100644 archivebox/core/admin.py
 create mode 100644 archivebox/core/apps.py
 rename archivebox/{ => core}/archive.py (95%)
 rename archivebox/{ => core}/archive_methods.py (99%)
 rename archivebox/{ => core}/config.py (100%)
 rename archivebox/{ => core}/index.py (97%)
 rename archivebox/{ => core}/links.py (96%)
 rename archivebox/{ => core}/logs.py (98%)
 create mode 100644 archivebox/core/management/commands/archivebox.py
 create mode 100644 archivebox/core/migrations/__init__.py
 create mode 100644 archivebox/core/models.py
 rename archivebox/{ => core}/parse.py (99%)
 rename archivebox/{ => core}/purge.py (93%)
 rename archivebox/{ => core}/schema.py (94%)
 create mode 100644 archivebox/core/tests.py
 rename archivebox/{ => core}/util.py (99%)
 create mode 100644 archivebox/core/views.py
 create mode 100755 archivebox/manage.py
 rename archivebox/{templates => }/static/archive.png (100%)
 rename archivebox/{templates => }/static/bootstrap.min.css (100%)
 rename archivebox/{templates => }/static/external.png (100%)
 rename archivebox/{templates => }/static/jquery.dataTables.min.css (100%)
 rename archivebox/{templates => }/static/jquery.dataTables.min.js (100%)
 rename archivebox/{templates => }/static/jquery.min.js (100%)
 rename archivebox/{templates => }/static/sort_asc.png (100%)
 rename archivebox/{templates => }/static/sort_both.png (100%)
 rename archivebox/{templates => }/static/sort_desc.png (100%)
 rename archivebox/{templates => }/static/spinner.gif (100%)

diff --git a/archivebox/__init__.py b/archivebox/__init__.py
index 0fb9e6f8..ab53f570 100644
--- a/archivebox/__init__.py
+++ b/archivebox/__init__.py
@@ -1,5 +1,5 @@
 
 
-__name__ = 'archivebox'
-__package__ = 'archivebox'
+#__name__ = 'archivebox'
+#__package__ = 'archivebox'
 
diff --git a/archivebox/archivebox/VERSION b/archivebox/archivebox/VERSION
new file mode 120000
index 00000000..6ff19de4
--- /dev/null
+++ b/archivebox/archivebox/VERSION
@@ -0,0 +1 @@
+../VERSION
\ No newline at end of file
diff --git a/archivebox/archivebox/__init__.py b/archivebox/archivebox/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/archivebox/settings.py b/archivebox/archivebox/settings.py
new file mode 100644
index 00000000..e027de02
--- /dev/null
+++ b/archivebox/archivebox/settings.py
@@ -0,0 +1,123 @@
+"""
+Django settings for archivebox project.
+
+Generated by 'django-admin startproject' using Django 2.1.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.1/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(COLLECTION_DIR, ...)
+REPO_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
+COLLECTION_DIR = os.path.abspath(os.curdir)
+
+print(REPO_DIR)
+print(COLLECTION_DIR)
+raise SystemExit(0)
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'm-ma!-z^0b5w4%**le#ig!7-d@h($t02q*96h*-ua+$lm9bvao'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+
+    'core',
+]
+
+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 = 'archivebox.urls'
+
+ACTIVE_THEME = 'default'
+TEMPLATES_DIR = os.path.join(REPO_DIR, 'themes', ACTIVE_THEME)
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [TEMPLATES_DIR],
+        '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 = 'archivebox.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(COLLECTION_DIR, 'database.sqlite3'),
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.1/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/2.1/topics/i18n/
+LANGUAGE_CODE = 'en-us'
+TIME_ZONE = 'UTC'
+USE_I18N = True
+USE_L10N = True
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.1/howto/static-files/
+STATIC_URL = '/static/'
diff --git a/archivebox/archivebox/urls.py b/archivebox/archivebox/urls.py
new file mode 100644
index 00000000..a077ec78
--- /dev/null
+++ b/archivebox/archivebox/urls.py
@@ -0,0 +1,21 @@
+"""archivebox URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/2.1/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
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+]
diff --git a/archivebox/archivebox/wsgi.py b/archivebox/archivebox/wsgi.py
new file mode 100644
index 00000000..f933afae
--- /dev/null
+++ b/archivebox/archivebox/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for archivebox 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/2.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'archivebox.settings')
+
+application = get_wsgi_application()
diff --git a/archivebox/core/__init__.py b/archivebox/core/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/core/admin.py b/archivebox/core/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/archivebox/core/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/archivebox/core/apps.py b/archivebox/core/apps.py
new file mode 100644
index 00000000..26f78a8e
--- /dev/null
+++ b/archivebox/core/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+    name = 'core'
diff --git a/archivebox/archive.py b/archivebox/core/archive.py
similarity index 95%
rename from archivebox/archive.py
rename to archivebox/core/archive.py
index b0a28428..e74b2644 100755
--- a/archivebox/archive.py
+++ b/archivebox/core/archive.py
@@ -16,11 +16,11 @@ import shutil
 
 from typing import List, Optional
 
-from .schema import Link
-from .links import links_after_timestamp
-from .index import write_links_index, load_links_index
-from .archive_methods import archive_link
-from .config import (
+from core.schema import Link
+from core.links import links_after_timestamp
+from core.index import write_links_index, load_links_index
+from core.archive_methods import archive_link
+from core.config import (
     ONLY_NEW,
     OUTPUT_DIR,
     VERSION,
@@ -41,12 +41,12 @@ from .config import (
     FETCH_GIT,
     FETCH_MEDIA,
 )
-from .util import (
+from core.util import (
     enforce_types,
     handle_stdin_import,
     handle_file_import,
 )
-from .logs import (
+from core.logs import (
     log_archiving_started,
     log_archiving_paused,
     log_archiving_finished,
@@ -142,7 +142,7 @@ def main(args=None) -> None:
                 "    If you're trying to create a new archive, you must run archivebox inside a completely empty directory."
                 "\n\n"
                 "    {lightred}Hint:{reset} To import a data folder created by an older version of ArchiveBox, \n"
-                "    just cd into the folder and run the archivebox comamnd to pick up where you left off.\n\n"
+                "    just cd into the folder and run the archivebox command to pick up where you left off.\n\n"
                 "    (Always make sure your data folder is backed up first before updating ArchiveBox)"
                 ).format(OUTPUT_DIR, **ANSI)
             )
diff --git a/archivebox/archive_methods.py b/archivebox/core/archive_methods.py
similarity index 99%
rename from archivebox/archive_methods.py
rename to archivebox/core/archive_methods.py
index d30d008d..add5a069 100644
--- a/archivebox/archive_methods.py
+++ b/archivebox/core/archive_methods.py
@@ -4,13 +4,13 @@ from typing import Dict, List, Tuple, Optional
 from collections import defaultdict
 from datetime import datetime
 
-from .schema import Link, ArchiveResult, ArchiveOutput
-from .index import (
+from core.schema import Link, ArchiveResult, ArchiveOutput
+from core.index import (
     write_link_index,
     patch_links_index,
     load_json_link_index,
 )
-from .config import (
+from core.config import (
     CURL_BINARY,
     GIT_BINARY,
     WGET_BINARY,
@@ -40,7 +40,7 @@ from .config import (
     YOUTUBEDL_VERSION,
     WGET_AUTO_COMPRESSION,
 )
-from .util import (
+from core.util import (
     enforce_types,
     domain,
     extension,
@@ -54,7 +54,7 @@ from .util import (
     chrome_args,
     run, PIPE, DEVNULL,
 )
-from .logs import (
+from core.logs import (
     log_link_archiving_started,
     log_link_archiving_finished,
     log_archive_method_started,
diff --git a/archivebox/config.py b/archivebox/core/config.py
similarity index 100%
rename from archivebox/config.py
rename to archivebox/core/config.py
diff --git a/archivebox/index.py b/archivebox/core/index.py
similarity index 97%
rename from archivebox/index.py
rename to archivebox/core/index.py
index b3cd350e..516e4304 100644
--- a/archivebox/index.py
+++ b/archivebox/core/index.py
@@ -5,8 +5,8 @@ from datetime import datetime
 from string import Template
 from typing import List, Tuple, Iterator, Optional, Mapping
 
-from .schema import Link, ArchiveResult
-from .config import (
+from core.schema import Link, ArchiveResult
+from core.config import (
     OUTPUT_DIR,
     TEMPLATES_DIR,
     VERSION,
@@ -14,7 +14,8 @@ from .config import (
     FOOTER_INFO,
     TIMEOUT,
 )
-from .util import (
+from core.util import (
+    ts_to_date,
     merge_links,
     urlencode,
     htmlencode,
@@ -26,9 +27,9 @@ from .util import (
     copy_and_overwrite,
     atomic_write,
 )
-from .parse import parse_links
-from .links import validate_links
-from .logs import (
+from core.parse import parse_links
+from core.links import validate_links
+from core.logs import (
     log_indexing_process_started,
     log_indexing_started,
     log_indexing_finished,
@@ -284,6 +285,7 @@ def write_html_link_index(link: Link, link_dir: Optional[str]=None) -> None:
         'tags': link.tags or 'untagged',
         'status': 'archived' if link.is_archived else 'not yet archived',
         'status_color': 'success' if link.is_archived else 'danger',
+        'oldest_archive_date': ts_to_date(link.oldest_archive_date),
     }
 
     html_index = Template(link_html).substitute(**template_vars)
diff --git a/archivebox/links.py b/archivebox/core/links.py
similarity index 96%
rename from archivebox/links.py
rename to archivebox/core/links.py
index 914c3575..fa4f53e6 100644
--- a/archivebox/links.py
+++ b/archivebox/core/links.py
@@ -1,14 +1,14 @@
 from typing import Iterable
 from collections import OrderedDict
 
-from .schema import Link
-from .util import (
+from core.schema import Link
+from core.util import (
     scheme,
     fuzzy_url,
     merge_links,
 )
 
-from .config import URL_BLACKLIST_PTN
+from core.config import URL_BLACKLIST_PTN
 
 
 def validate_links(links: Iterable[Link]) -> Iterable[Link]:
diff --git a/archivebox/logs.py b/archivebox/core/logs.py
similarity index 98%
rename from archivebox/logs.py
rename to archivebox/core/logs.py
index d9b92422..0b9243c2 100644
--- a/archivebox/logs.py
+++ b/archivebox/core/logs.py
@@ -5,8 +5,8 @@ from datetime import datetime
 from dataclasses import dataclass
 from typing import Optional
 
-from .schema import Link, ArchiveResult
-from .config import ANSI, OUTPUT_DIR
+from core.schema import Link, ArchiveResult
+from core.config import ANSI, OUTPUT_DIR
 
 
 @dataclass
diff --git a/archivebox/core/management/commands/archivebox.py b/archivebox/core/management/commands/archivebox.py
new file mode 100644
index 00000000..1764e4e2
--- /dev/null
+++ b/archivebox/core/management/commands/archivebox.py
@@ -0,0 +1,10 @@
+from django.core.management.base import BaseCommand
+
+
+from core.archive import main
+
+class Command(BaseCommand):
+    help = 'ArchiveBox test.bee'
+
+    def handle(self, *args, **kwargs):
+        main()
diff --git a/archivebox/core/migrations/__init__.py b/archivebox/core/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/core/models.py b/archivebox/core/models.py
new file mode 100644
index 00000000..71a83623
--- /dev/null
+++ b/archivebox/core/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/archivebox/parse.py b/archivebox/core/parse.py
similarity index 99%
rename from archivebox/parse.py
rename to archivebox/core/parse.py
index 49ffa7fd..9a6936c0 100644
--- a/archivebox/parse.py
+++ b/archivebox/core/parse.py
@@ -24,8 +24,8 @@ from typing import Tuple, List, IO, Iterable
 from datetime import datetime
 import xml.etree.ElementTree as etree
 
-from .config import TIMEOUT
-from .util import (
+from core.config import TIMEOUT
+from core.util import (
     htmldecode,
     str_between,
     URL_REGEX,
diff --git a/archivebox/purge.py b/archivebox/core/purge.py
similarity index 93%
rename from archivebox/purge.py
rename to archivebox/core/purge.py
index ddc64b6b..d9a5deda 100755
--- a/archivebox/purge.py
+++ b/archivebox/core/purge.py
@@ -6,8 +6,8 @@ from os.path import exists, join
 from shutil import rmtree
 from typing import List
 
-from .config import ARCHIVE_DIR, OUTPUT_DIR
-from .index import parse_json_links_index, write_html_links_index, write_json_links_index
+from core.config import ARCHIVE_DIR, OUTPUT_DIR
+from core.index import parse_json_links_index, write_html_links_index, write_json_links_index
 
 
 def cleanup_index(regexes: List[str], proceed: bool, delete: bool) -> None:
diff --git a/archivebox/schema.py b/archivebox/core/schema.py
similarity index 94%
rename from archivebox/schema.py
rename to archivebox/core/schema.py
index a4d3a836..c2da775d 100644
--- a/archivebox/schema.py
+++ b/archivebox/core/schema.py
@@ -221,28 +221,20 @@ class Link:
         return ts_to_date(self.updated) if self.updated else None
 
     @property
-    def oldest_archive_date(self) -> Optional[datetime]:
-        from .util import ts_to_date
+    def archive_dates(self) -> List[datetime]:
+        return [
+            result.start_ts
+            for method in self.history.keys()
+                for result in self.history[method]
+        ]
 
-        most_recent = min(
-            (ts_to_date(result.start_ts)
-             for method in self.history.keys()
-                for result in self.history[method]),
-            default=None,
-        )
-        return ts_to_date(most_recent) if most_recent else None
+    @property
+    def oldest_archive_date(self) -> Optional[datetime]:
+        return min(self.archive_dates, default=None)
 
     @property
     def newest_archive_date(self) -> Optional[datetime]:
-        from .util import ts_to_date
-
-        most_recent = max(
-            (ts_to_date(result.start_ts)
-             for method in self.history.keys()
-                for result in self.history[method]),
-            default=None,
-        )
-        return ts_to_date(most_recent) if most_recent else None
+        return max(self.archive_dates, default=None)
 
     ### Archive Status Helpers
     @property
diff --git a/archivebox/core/tests.py b/archivebox/core/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/archivebox/core/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/archivebox/util.py b/archivebox/core/util.py
similarity index 99%
rename from archivebox/util.py
rename to archivebox/core/util.py
index ec8c256b..cf314287 100644
--- a/archivebox/util.py
+++ b/archivebox/core/util.py
@@ -26,8 +26,8 @@ from subprocess import (
 
 from base32_crockford import encode as base32_encode         # type: ignore
 
-from .schema import Link
-from .config import (
+from core.schema import Link
+from core.config import (
     ANSI,
     TERM_WIDTH,
     SOURCES_DIR,
@@ -40,7 +40,7 @@ from .config import (
     CHROME_OPTIONS,
     PYTHON_DIR,
 )
-from .logs import pretty_path
+from core.logs import pretty_path
 
 ### Parsing Helpers
 
@@ -62,17 +62,17 @@ base_url = lambda url: without_scheme(url)  # uniq base url used to dedupe links
 without_www = lambda url: url.replace('://www.', '://', 1)
 without_trailing_slash = lambda url: url[:-1] if url[-1] == '/' else url.replace('/?', '?')
 fuzzy_url = lambda url: without_trailing_slash(without_www(without_scheme(url.lower())))
-
-short_ts = lambda ts: str(parse_date(ts).timestamp()).split('.')[0]
-ts_to_date = lambda ts: parse_date(ts).strftime('%Y-%m-%d %H:%M')
-ts_to_iso = lambda ts: parse_date(ts).isoformat()
+hashurl = lambda url: base32_encode(int(sha256(base_url(url).encode('utf-8')).hexdigest(), 16))[:20]
 
 urlencode = lambda s: s and quote(s, encoding='utf-8', errors='replace')
 urldecode = lambda s: s and unquote(s)
 htmlencode = lambda s: s and escape(s, quote=True)
 htmldecode = lambda s: s and unescape(s)
 
-hashurl = lambda url: base32_encode(int(sha256(base_url(url).encode('utf-8')).hexdigest(), 16))[:20]
+short_ts = lambda ts: str(parse_date(ts).timestamp()).split('.')[0]
+ts_to_date = lambda ts: ts and parse_date(ts).strftime('%Y-%m-%d %H:%M')
+ts_to_iso = lambda ts: ts and parse_date(ts).isoformat()
+
 
 URL_REGEX = re.compile(
     r'http[s]?://'                    # start matching from allowed schemes
@@ -357,11 +357,11 @@ def str_between(string: str, start: str, end: str=None) -> str:
 def parse_date(date: Any) -> Optional[datetime]:
     """Parse unix timestamps, iso format, and human-readable strings"""
     
-    if isinstance(date, datetime):
-        return date
-
     if date is None:
         return None
+
+    if isinstance(date, datetime):
+        return date
     
     if isinstance(date, (float, int)):
         date = str(date)
diff --git a/archivebox/core/views.py b/archivebox/core/views.py
new file mode 100644
index 00000000..91ea44a2
--- /dev/null
+++ b/archivebox/core/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/archivebox/manage.py b/archivebox/manage.py
new file mode 100755
index 00000000..cc70dfd5
--- /dev/null
+++ b/archivebox/manage.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == '__main__':
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'archivebox.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)
diff --git a/archivebox/templates/static/archive.png b/archivebox/static/archive.png
similarity index 100%
rename from archivebox/templates/static/archive.png
rename to archivebox/static/archive.png
diff --git a/archivebox/templates/static/bootstrap.min.css b/archivebox/static/bootstrap.min.css
similarity index 100%
rename from archivebox/templates/static/bootstrap.min.css
rename to archivebox/static/bootstrap.min.css
diff --git a/archivebox/templates/static/external.png b/archivebox/static/external.png
similarity index 100%
rename from archivebox/templates/static/external.png
rename to archivebox/static/external.png
diff --git a/archivebox/templates/static/jquery.dataTables.min.css b/archivebox/static/jquery.dataTables.min.css
similarity index 100%
rename from archivebox/templates/static/jquery.dataTables.min.css
rename to archivebox/static/jquery.dataTables.min.css
diff --git a/archivebox/templates/static/jquery.dataTables.min.js b/archivebox/static/jquery.dataTables.min.js
similarity index 100%
rename from archivebox/templates/static/jquery.dataTables.min.js
rename to archivebox/static/jquery.dataTables.min.js
diff --git a/archivebox/templates/static/jquery.min.js b/archivebox/static/jquery.min.js
similarity index 100%
rename from archivebox/templates/static/jquery.min.js
rename to archivebox/static/jquery.min.js
diff --git a/archivebox/templates/static/sort_asc.png b/archivebox/static/sort_asc.png
similarity index 100%
rename from archivebox/templates/static/sort_asc.png
rename to archivebox/static/sort_asc.png
diff --git a/archivebox/templates/static/sort_both.png b/archivebox/static/sort_both.png
similarity index 100%
rename from archivebox/templates/static/sort_both.png
rename to archivebox/static/sort_both.png
diff --git a/archivebox/templates/static/sort_desc.png b/archivebox/static/sort_desc.png
similarity index 100%
rename from archivebox/templates/static/sort_desc.png
rename to archivebox/static/sort_desc.png
diff --git a/archivebox/templates/static/spinner.gif b/archivebox/static/spinner.gif
similarity index 100%
rename from archivebox/templates/static/spinner.gif
rename to archivebox/static/spinner.gif
diff --git a/requirements.txt b/requirements.txt
index 6c12aee4..42fba851 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+django
 base32-crockford
 
 setuptools