more, add initial mod ui
This commit is contained in:
parent
6f01bac76c
commit
c677cebde5
@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.3 on 2016-11-16 14:37
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('frontend', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModPriv',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('board', models.CharField(default='all', max_length=128)),
|
||||||
|
('level', models.IntegerField(default=3)),
|
||||||
|
('pubkey', models.CharField(editable=False, max_length=256)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.3 on 2016-11-16 14:54
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from nntpchan.frontend import util
|
||||||
|
|
||||||
|
def make_admin_key(apps, schema_editor):
|
||||||
|
ModPriv = apps.get_model("frontend", "ModPriv")
|
||||||
|
sk, pk = util.keygen()
|
||||||
|
print("!!!!! YOUR ADMIN SECRET KEY IS {} !!!!!".format(sk))
|
||||||
|
mod = ModPriv(level=0, pubkey=pk)
|
||||||
|
mod.save()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('frontend', '0002_modpriv'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(make_admin_key)
|
||||||
|
]
|
@ -8,6 +8,8 @@ from . import util
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
class Attachment(models.Model):
|
class Attachment(models.Model):
|
||||||
"""
|
"""
|
||||||
a file attachment assiciated with a post
|
a file attachment assiciated with a post
|
||||||
@ -24,12 +26,19 @@ class Attachment(models.Model):
|
|||||||
ext = self.filename.split('.')[-1]
|
ext = self.filename.split('.')[-1]
|
||||||
return '{}.{}'.format(self.filehash, ext)
|
return '{}.{}'.format(self.filehash, ext)
|
||||||
|
|
||||||
def thumb(self):
|
def thumb(self, root=settings.MEDIA_URL):
|
||||||
return '{}thumb-{}.jpg'.format(settings.MEDIA_URL, self.path())
|
return '{}thumb-{}.jpg'.format(root, self.path())
|
||||||
|
|
||||||
def source(self):
|
def source(self, root=settings.MEDIA_URL):
|
||||||
return '{}{}'.format(settings.MEDIA_URL, self.path())
|
return '{}{}'.format(root, self.path())
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
remove from filesystem and delete self
|
||||||
|
"""
|
||||||
|
os.unlink(os.path.join(settings.MEDIA_ROOT, self.thumb('')))
|
||||||
|
os.unlink(os.path.join(settings.MEDIA_ROOT, self.source('')))
|
||||||
|
self.delete()
|
||||||
|
|
||||||
class Newsgroup(models.Model):
|
class Newsgroup(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -58,23 +67,33 @@ class Post(models.Model):
|
|||||||
name = models.CharField(max_length=256, default='Anonymous')
|
name = models.CharField(max_length=256, default='Anonymous')
|
||||||
pubkey = models.CharField(max_length=64, default='')
|
pubkey = models.CharField(max_length=64, default='')
|
||||||
signature = models.CharField(max_length=64, default='')
|
signature = models.CharField(max_length=64, default='')
|
||||||
newsgroup = models.ForeignKey(Newsgroup, on_delete=models.CASCADE)
|
newsgroup = models.ForeignKey(Newsgroup)
|
||||||
attachments = models.ManyToManyField(Attachment)
|
attachments = models.ManyToManyField(Attachment)
|
||||||
posted = models.IntegerField(default=0)
|
posted = models.IntegerField(default=0)
|
||||||
placeholder = models.BooleanField(default=False)
|
placeholder = models.BooleanField(default=False)
|
||||||
last_bumped = models.IntegerField(default=0)
|
last_bumped = models.IntegerField(default=0)
|
||||||
|
|
||||||
def has_attachment(self, filehash):
|
def has_attachment(self, filehash):
|
||||||
|
"""
|
||||||
|
return True if we own a file attachment by its hash
|
||||||
|
"""
|
||||||
for att in self.attachments.all():
|
for att in self.attachments.all():
|
||||||
if att.filehash == filehash:
|
if att.filehash in filehash:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_all_replies(self):
|
def get_all_replies(self):
|
||||||
|
"""
|
||||||
|
get all replies to this thread
|
||||||
|
"""
|
||||||
if self.is_op():
|
if self.is_op():
|
||||||
return Post.objects.filter(reference=self.msgid).order_by('posted')
|
return Post.objects.filter(reference=self.msgid).order_by('posted')
|
||||||
|
|
||||||
def get_board_replies(self, truncate=5):
|
def get_board_replies(self, truncate=5):
|
||||||
|
"""
|
||||||
|
get replies to this thread
|
||||||
|
truncate to last N replies
|
||||||
|
"""
|
||||||
rpls = self.get_all_replies()
|
rpls = self.get_all_replies()
|
||||||
l = len(rpls)
|
l = len(rpls)
|
||||||
if l > truncate:
|
if l > truncate:
|
||||||
@ -91,6 +110,9 @@ class Post(models.Model):
|
|||||||
return datetime.fromtimestamp(self.posted)
|
return datetime.fromtimestamp(self.posted)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
|
"""
|
||||||
|
self explainitory
|
||||||
|
"""
|
||||||
if self.is_op():
|
if self.is_op():
|
||||||
op = util.hashid(self.msgid)
|
op = util.hashid(self.msgid)
|
||||||
return reverse('frontend:thread', args=[op])
|
return reverse('frontend:thread', args=[op])
|
||||||
@ -100,5 +122,100 @@ class Post(models.Model):
|
|||||||
return reverse('frontend:thread', args=[op]) + '#{}'.format(frag)
|
return reverse('frontend:thread', args=[op]) + '#{}'.format(frag)
|
||||||
|
|
||||||
def bump(self, last):
|
def bump(self, last):
|
||||||
|
"""
|
||||||
|
bump thread
|
||||||
|
"""
|
||||||
if self.is_op():
|
if self.is_op():
|
||||||
self.last_bumped = last
|
self.last_bumped = last
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
remove post and all attachments
|
||||||
|
"""
|
||||||
|
for att in self.attachments.all():
|
||||||
|
att.remove()
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
class ModPriv(models.Model):
|
||||||
|
"""
|
||||||
|
a record that permits moderation actions on certain boards or globally
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
absolute power :^DDDDDDD (does not exist)
|
||||||
|
"""
|
||||||
|
GOD = 0
|
||||||
|
|
||||||
|
"""
|
||||||
|
node admin
|
||||||
|
"""
|
||||||
|
ADMIN = 1
|
||||||
|
|
||||||
|
"""
|
||||||
|
can ban, delete and edit posts
|
||||||
|
"""
|
||||||
|
MOD = 2
|
||||||
|
|
||||||
|
"""
|
||||||
|
can only delete
|
||||||
|
"""
|
||||||
|
JANITOR = 3
|
||||||
|
|
||||||
|
"""
|
||||||
|
lowest access level for login
|
||||||
|
"""
|
||||||
|
LOWEST = JANITOR
|
||||||
|
|
||||||
|
"""
|
||||||
|
what board this priviledge is for or 'all' for global
|
||||||
|
"""
|
||||||
|
board = models.CharField(max_length=128, default='all')
|
||||||
|
|
||||||
|
"""
|
||||||
|
what level of priviledge is granted
|
||||||
|
"""
|
||||||
|
level = models.IntegerField(default=3)
|
||||||
|
|
||||||
|
"""
|
||||||
|
public key of mod mod user
|
||||||
|
"""
|
||||||
|
pubkey = models.CharField(max_length=256, editable=False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def has_access(level, pubkey, board_name=None):
|
||||||
|
# check global priviledge
|
||||||
|
global_priv = ModPriv.objects.filter(pubkey=pubkey, board='all')
|
||||||
|
for priv in global_priv:
|
||||||
|
if priv.level <= level:
|
||||||
|
return True
|
||||||
|
# check board level priviledge
|
||||||
|
if board_name:
|
||||||
|
board_priv = ModPriv.objects.filter(pubkey=pubkey, board=board_name)
|
||||||
|
for priv in board_priv:
|
||||||
|
if priv.level <= level:
|
||||||
|
return True
|
||||||
|
# no access allowed
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def try_delete(pubkey, post):
|
||||||
|
"""
|
||||||
|
try deleting a post, return True if it was deleted otherwise return False
|
||||||
|
"""
|
||||||
|
if ModPriv.has_access(ModPriv.JANITOR, pubkey, post.newsgroup.name):
|
||||||
|
# we can do it
|
||||||
|
post.remove()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def try_edit(pubkey, post, newbody):
|
||||||
|
"""
|
||||||
|
try editing a post by replacing its body with a new one
|
||||||
|
returns True if this was done otherwise return False
|
||||||
|
"""
|
||||||
|
if ModPriv.has_access(ModPriv.MOD, pubkey, post.newsgroup.name):
|
||||||
|
post.message = newbody
|
||||||
|
post.save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
@ -4,8 +4,9 @@ from . import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^ctl-(?P<page>[0-9]+)\.html$', views.modlog, name='old-modlog'),
|
url(r'^ctl-(?P<page>[0-9]+)\.html$', views.modlog, name='old-modlog'),
|
||||||
url(r'^mod/$', views.modlog, name='modlog'),
|
url(r'^mod/$', views.ModView.as_view(), name='mod'),
|
||||||
url(r'^mod/(?P<page>[0-9]+)$', views.modlog, name='modlog-page'),
|
url(r'^mod/keygen', views.keygen, name='keygen'),
|
||||||
|
url(r'^mod/(?P<page>[0-9]+)$', views.modlog, name='mod-page'),
|
||||||
url(r'^overchan\.(?P<name>[a-zA-Z0-9\.]+)-(?P<page>[0-9]+)\.html$', views.BoardView.as_view(), name='old-board'),
|
url(r'^overchan\.(?P<name>[a-zA-Z0-9\.]+)-(?P<page>[0-9]+)\.html$', views.BoardView.as_view(), name='old-board'),
|
||||||
url(r'^overchan\.(?P<name>[a-zA-Z0-9\.]+)/', views.BoardView.as_view(), name='board-alt'),
|
url(r'^overchan\.(?P<name>[a-zA-Z0-9\.]+)/', views.BoardView.as_view(), name='board-alt'),
|
||||||
url(r'^thread-(?P<op>[a-fA-F0-9\.]{40})\.html$', views.ThreadView.as_view(), name='old-thread'),
|
url(r'^thread-(?P<op>[a-fA-F0-9\.]{40})\.html$', views.ThreadView.as_view(), name='old-thread'),
|
||||||
|
@ -14,6 +14,18 @@ import random
|
|||||||
import nntplib
|
import nntplib
|
||||||
import email.message
|
import email.message
|
||||||
|
|
||||||
|
|
||||||
|
def keygen():
|
||||||
|
"""
|
||||||
|
generate a new keypair
|
||||||
|
"""
|
||||||
|
k = nacl.signing.SigningKey.generate()
|
||||||
|
return hexlify(k.encode()).decode('ascii'), hexlify(k.verify_key.encode()).decode('ascii')
|
||||||
|
|
||||||
|
def to_public(sk):
|
||||||
|
k = nacl.signing.SigningKey(sk, nacl.signing.encoding.HexEncoder)
|
||||||
|
return hexlify(k.verify_key.encode()).decode('ascii')
|
||||||
|
|
||||||
def hashid(msgid):
|
def hashid(msgid):
|
||||||
h = hashlib.sha1()
|
h = hashlib.sha1()
|
||||||
m = '{}'.format(msgid).encode('ascii')
|
m = '{}'.format(msgid).encode('ascii')
|
||||||
@ -80,7 +92,7 @@ def createPost(newsgroup, ref, form, files, secretKey=None):
|
|||||||
msg['Content-Type'] = 'text/plain; charset=UTF-8'
|
msg['Content-Type'] = 'text/plain; charset=UTF-8'
|
||||||
m = '{}'.format(form['message'] or ' ')
|
m = '{}'.format(form['message'] or ' ')
|
||||||
msg.set_payload(m)
|
msg.set_payload(m)
|
||||||
msg['Message-Id'] = '<{}${}@signed.{}>'.format(randstr(5), int(time_int(datetime.now())), settings.FRONTEND_NAME)
|
msg['Message-Id'] = '<{}${}@{}>'.format(randstr(5), int(time_int(datetime.now())), settings.FRONTEND_NAME)
|
||||||
if secretKey:
|
if secretKey:
|
||||||
msg['Path'] = settings.FRONTEND_NAME
|
msg['Path'] = settings.FRONTEND_NAME
|
||||||
# sign
|
# sign
|
||||||
|
@ -5,7 +5,7 @@ from django.shortcuts import render, get_object_or_404
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
|
||||||
|
|
||||||
from .models import Post, Newsgroup
|
from .models import Post, Newsgroup, ModPriv
|
||||||
|
|
||||||
from captcha.image import ImageCaptcha
|
from captcha.image import ImageCaptcha
|
||||||
from . import util
|
from . import util
|
||||||
@ -54,6 +54,11 @@ class Postable:
|
|||||||
'error' : 'invalid captcha',
|
'error' : 'invalid captcha',
|
||||||
'only_mod': False
|
'only_mod': False
|
||||||
}
|
}
|
||||||
|
# bypass captcha for mod users
|
||||||
|
mod = None
|
||||||
|
if 'mod' in request.session:
|
||||||
|
mod = request.session['mod']
|
||||||
|
if mod is None:
|
||||||
solution = request.session['captcha']
|
solution = request.session['captcha']
|
||||||
if solution is not None:
|
if solution is not None:
|
||||||
if 'captcha' in request.POST:
|
if 'captcha' in request.POST:
|
||||||
@ -65,6 +70,10 @@ class Postable:
|
|||||||
else:
|
else:
|
||||||
ctx['msgid'], ctx['error'] = self.handle_post(request, **kwargs)
|
ctx['msgid'], ctx['error'] = self.handle_post(request, **kwargs)
|
||||||
request.session['captcha'] = ''
|
request.session['captcha'] = ''
|
||||||
|
else:
|
||||||
|
# we have a mod session
|
||||||
|
ctx['msgid'], ctx['error'] = self.handle_post(request, **kwargs)
|
||||||
|
|
||||||
request.session.save()
|
request.session.save()
|
||||||
code = 201
|
code = 201
|
||||||
if ctx['error']:
|
if ctx['error']:
|
||||||
@ -147,6 +156,65 @@ class FrontPageView(generic.View):
|
|||||||
return render(request, self.template_name, ctx)
|
return render(request, self.template_name, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
class ModView(generic.View, Postable):
|
||||||
|
|
||||||
|
def show_login(self, request):
|
||||||
|
"""
|
||||||
|
handle login page
|
||||||
|
"""
|
||||||
|
request.session['mod'] = None
|
||||||
|
request.session.save()
|
||||||
|
return render(request, 'frontend/modlogin.html')
|
||||||
|
|
||||||
|
def logout(self, request):
|
||||||
|
request.session['mod'] = None
|
||||||
|
request.session.save()
|
||||||
|
return render(request, 'frontend/redirect.html', { 'url' : reverse('frontend:mod')})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
action = None
|
||||||
|
if 'action' in request.POST:
|
||||||
|
action = request.POST['action']
|
||||||
|
mod = None
|
||||||
|
msg = 'bad login'
|
||||||
|
if 'mod' in request.session:
|
||||||
|
mod = request.session['mod']
|
||||||
|
if action == 'logout':
|
||||||
|
return self.logout(request)
|
||||||
|
if action == 'login' and 'secret' in request.POST and mod is None:
|
||||||
|
# try login
|
||||||
|
sk = request.POST['secret']
|
||||||
|
pk = None
|
||||||
|
try:
|
||||||
|
pk = util.to_public(sk)
|
||||||
|
print (pk)
|
||||||
|
except:
|
||||||
|
msg = 'bad key format'
|
||||||
|
if pk is not None and ModPriv.has_access(ModPriv.LOWEST, pk):
|
||||||
|
mod = request.session['mod'] = {'sk' : sk, 'pk' : pk}
|
||||||
|
request.session.save()
|
||||||
|
# login okay
|
||||||
|
return render(request, 'frontend/redirect.html', {'url' : reverse('frontend:mod'), 'msg' : 'logged in'})
|
||||||
|
if mod is None:
|
||||||
|
# no mod session
|
||||||
|
return render(request, 'frontend/redirect.html', {'url' : reverse('frontend:mod'), 'msg' : msg } )
|
||||||
|
else:
|
||||||
|
# do mod action
|
||||||
|
return self.handle_mod_action(request)
|
||||||
|
|
||||||
|
def handle_mod_action(self, request):
|
||||||
|
return render(request, 'frontend/redirect.html', {'url' : reverse('frontend:mod')} )
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
mod = None
|
||||||
|
if 'mod' in request.session:
|
||||||
|
mod = request.session['mod']
|
||||||
|
if mod:
|
||||||
|
return render(request, 'frontend/modpage.html', {'mod' : mod })
|
||||||
|
else:
|
||||||
|
return self.show_login(request)
|
||||||
|
|
||||||
|
|
||||||
def modlog(request, page=None):
|
def modlog(request, page=None):
|
||||||
page = int(page or '0')
|
page = int(page or '0')
|
||||||
ctx = {
|
ctx = {
|
||||||
@ -171,3 +239,11 @@ def create_captcha(request):
|
|||||||
r =HttpResponse(c)
|
r =HttpResponse(c)
|
||||||
r['Content-Type'] = 'image/png'
|
r['Content-Type'] = 'image/png'
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def keygen(request):
|
||||||
|
"""
|
||||||
|
generate new keypair
|
||||||
|
"""
|
||||||
|
ctx = {}
|
||||||
|
ctx['sk'], ctx['pk'] = util.keygen()
|
||||||
|
return render(request, 'frontend/keygen.html', ctx)
|
||||||
|
@ -77,7 +77,7 @@ DATABASES = {
|
|||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
'HOST': '/var/run/postgresql',
|
'HOST': '/var/run/postgresql',
|
||||||
'NAME': 'postgres',
|
'NAME': 'jeff',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
{% extends "frontend/base.html" %}
|
||||||
|
{% block title %} new tripcode keypair {% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<pre class="tripcode-outer">public key: {{pk}}
|
||||||
|
secret key: {{sk}}
|
||||||
|
</pre>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,11 @@
|
|||||||
|
{% extends "frontend/base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label for="secret"> secret key </label>
|
||||||
|
<input type="password" id="secret" name="secret"></input>
|
||||||
|
<input type="hidden" name="action" value="login"></input>
|
||||||
|
<input type="submit" value="login"></input>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "frontend/base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="logout"></input>
|
||||||
|
<input type="submit" value="logout"></input>
|
||||||
|
</form>
|
||||||
|
mod page
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "frontend/base.html" %}
|
||||||
|
{% block head %}
|
||||||
|
<meta http-equiv="refresh" content="1; {{url}}" />
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<pre>{{msg}}</pre>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "frontend/base.html" %}
|
||||||
|
{% block title %} new tripcode keypair {% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="tripcode-outer">
|
||||||
|
<div class="tripcode">
|
||||||
|
public key:
|
||||||
|
<span class="tripcode-public">{{pk}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tripcode">
|
||||||
|
secret key:
|
||||||
|
<span class="tripcode-secret">{{sk}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -21,6 +21,6 @@ from . import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^webhook$', views.webhook),
|
url(r'^webhook$', views.webhook),
|
||||||
url(r'^nntpchan/', include('nntpchan.frontend.urls', namespace='frontend'), name='nntpchan'),
|
url(r'^nntpchan/', include('nntpchan.frontend.urls', namespace='frontend'), name='nntpchan')
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
urlpatterns.append(url(r'', views.frontpage, name='frontpage'))
|
urlpatterns.append(url(r'', views.frontpage, name='frontpage'))
|
||||||
|
@ -139,3 +139,5 @@ def webhook(request):
|
|||||||
return JsonResponse({ 'error': '{}'.format(ex) })
|
return JsonResponse({ 'error': '{}'.format(ex) })
|
||||||
else:
|
else:
|
||||||
return JsonResponse({'posted': True})
|
return JsonResponse({'posted': True})
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@ suggested setup is with pyvenv
|
|||||||
|
|
||||||
for a dev server, run:
|
for a dev server, run:
|
||||||
|
|
||||||
|
cd nntpchan
|
||||||
python3 -m venv v
|
python3 -m venv v
|
||||||
v/bin/pip install -r requirements.txt
|
v/bin/pip install -r requirements.txt
|
||||||
cd nntpchan
|
v/bin/python manage.py migrate
|
||||||
../v/bin/python manage.py migrate
|
v/bin/pyrhon manage.py runserver
|
||||||
../v/bin/pyrhon manage.py runserver
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user