Initial version of django-based tool to administer blog registrations,
authorMagnus Hagander <magnus@hagander.net>
Mon, 3 Nov 2008 15:53:57 +0000 (15:53 +0000)
committerMagnus Hagander <magnus@hagander.net>
Mon, 3 Nov 2008 15:53:57 +0000 (15:53 +0000)
both as a subscriber directly and as a planet administrator.

More to be done, not in the least in the design department, but getting
it in there so I don't delete it - and so that others can work on the
all-important design stuff!

15 files changed:
planetadmin/__init__.py [new file with mode: 0644]
planetadmin/auth.py [new file with mode: 0644]
planetadmin/local_settings.py.sample [new file with mode: 0644]
planetadmin/manage.py [new file with mode: 0755]
planetadmin/register/__init__.py [new file with mode: 0644]
planetadmin/register/models.py [new file with mode: 0644]
planetadmin/register/templates/blogposts.html [new file with mode: 0644]
planetadmin/register/templates/index.html [new file with mode: 0644]
planetadmin/register/templates/regbase.html [new file with mode: 0644]
planetadmin/register/templates/registration/login.html [new file with mode: 0644]
planetadmin/register/urls.py [new file with mode: 0644]
planetadmin/register/views.py [new file with mode: 0644]
planetadmin/settings.py [new file with mode: 0644]
planetadmin/urls.py [new file with mode: 0644]
template/base.tmpl

diff --git a/planetadmin/__init__.py b/planetadmin/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/planetadmin/auth.py b/planetadmin/auth.py
new file mode 100644 (file)
index 0000000..3fed0b4
--- /dev/null
@@ -0,0 +1,29 @@
+from django.contrib.auth.models import User
+import psycopg2
+
+class AuthBackend:
+       def authenticate(self, username=None, password=None):
+               conn = psycopg2.connect('host=wwwmaster.postgresql.org dbname=186_www user=auth_svc password=g7y3m9u8 sslmode=require')
+               try:
+                       conn.set_client_encoding('UNICODE')
+                       cur = conn.cursor()
+                       cur.execute('SELECT * FROM community_login(%s,%s)', (username, password))
+                       row  = cur.fetchall()[0]
+               finally:
+                       conn.close()
+
+               if row[1] == 1:
+                       try:
+                               user = User.objects.get(username=username)
+                       except User.DoesNotExist:
+                               # User doesn't exist yet
+                               user = User(username=username, password='setmanually', email=row[3], first_name=row[2])
+                               user.save()
+                       return user
+               return None
+
+       def get_user(self, user_id):
+               try:
+                       return User.objects.get(pk=user_id)
+               except User.DoesNotExist:
+                       return None
diff --git a/planetadmin/local_settings.py.sample b/planetadmin/local_settings.py.sample
new file mode 100644 (file)
index 0000000..0107c11
--- /dev/null
@@ -0,0 +1,6 @@
+# Add any settings here to override the default ones
+
+DATABASE_NAME = 'planetbeta'
+DATABASE_USER = 'planetadmin'
+
+DEBUG = True
diff --git a/planetadmin/manage.py b/planetadmin/manage.py
new file mode 100755 (executable)
index 0000000..5e78ea9
--- /dev/null
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+    import settings # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)
diff --git a/planetadmin/register/__init__.py b/planetadmin/register/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/planetadmin/register/models.py b/planetadmin/register/models.py
new file mode 100644 (file)
index 0000000..931a854
--- /dev/null
@@ -0,0 +1,50 @@
+from django.db import models
+
+class Blog(models.Model):
+       feedurl = models.CharField(max_length=255, blank=False)
+       name = models.CharField(max_length=255, blank=False)
+       blogurl = models.CharField(max_length=255, blank=False)
+       lastget = models.DateTimeField(default='2000-01-01')
+       userid = models.CharField(max_length=255, blank=False)
+       approved = models.BooleanField()
+
+       def __str__(self):
+               return self.feedurl
+
+       @property
+       def approved_(self):
+               if self.approved:
+                       return "Yes"
+               return "No"
+
+       class Meta:
+               db_table = 'planet\".\"feeds'
+               ordering = ['approved','name']
+
+       class Admin:
+               pass
+
+class Post(models.Model):
+       feed = models.ForeignKey(Blog,db_column='feed')
+       guid = models.CharField(max_length=255)
+       link = models.CharField(max_length=255)
+       txt = models.CharField(max_length=255)
+       dat = models.DateTimeField()
+       title = models.CharField(max_length=255)
+       guidisperma = models.BooleanField()
+       hidden = models.BooleanField()
+
+       def __str__(self):
+               return self.title
+
+       def visible(self):
+               if self.hidden:
+                       return "Hidden"
+               return "Visible"
+
+       class Meta:
+               db_table = 'planet\".\"posts'
+               ordering = ['-dat']
+
+       class Admin:
+               pass
diff --git a/planetadmin/register/templates/blogposts.html b/planetadmin/register/templates/blogposts.html
new file mode 100644 (file)
index 0000000..42db49b
--- /dev/null
@@ -0,0 +1,32 @@
+{% extends "regbase.html" %}
+{%block regcontent %}
+<p>
+This is a list of the blogposts we have found in your feed.
+Please note that if you delete a post, it will be automatically
+re-added if it's still in your RSS. This can be used to force
+planet to fetch a new version of the post. If you want the post
+not to show up, it should stay in planet and be hidden.
+</p>
+<p>
+Return to <a href="../..">blog list</a>.
+</p>
+<table border="1" cellspacing="0" cellpadding="1">
+<tr>
+ <th>Date</th>
+ <th>Title</th>
+ <th>Status</th>
+ <th>Operation</th>
+</tr>
+{% for post in posts %}
+<tr valign="top">
+ <td>{{post.dat}}</td>
+ <td>{{post.title}}</td>
+ <td>{{post.visible}}</td>
+ <td>
+  {%if post.hidden%}<a href="unhide/{{post.id}}/">Unhide</a>{%else%}<a href="hide/{{post.id}}/">Hide</a>{%endif%}<br/>
+  <a href="delete/{{post.id}}/">Delete</a>
+ </td>
+</tr>
+{%endfor%}
+</table>
+{% endblock %}
diff --git a/planetadmin/register/templates/index.html b/planetadmin/register/templates/index.html
new file mode 100644 (file)
index 0000000..fcc2271
--- /dev/null
@@ -0,0 +1,72 @@
+{% extends "regbase.html" %}
+{%block regcontent%}
+{%if blogs %}
+<p>
+We have the following blog(s) registered:
+</p>
+<table border="1" cellspacing="0" cellpadding="1">
+<tr>
+ <th>Userid</th>
+ <th>Name</th>
+ <th>Approved</th>
+ <th>Feed URL/Blog URL</th>
+ <th>Operations</th>
+{% if user.is_superuser %}
+ <th>Admin</th>
+{%endif%}
+</tr>
+{%for blog in blogs%}
+<tr valign="top" {%if user.is_superuser and not blog.approved%}bgcolor="red"{%endif%}>
+ <td>{{blog.userid}}</td>
+ <td>{%if user.is_superuser %}
+<form method="post" action="modify/{{blog.id}}/">
+<input type="text" name="blogname" value="{{blog.name}}">
+<input type="submit" value="Save">
+</form>
+{%else%}
+{{blog.name}}
+{%endif%}
+ </td>
+ <td>{{blog.approved_}}</td>
+ <td>{{blog.feedurl}}<br/>{{blog.blogurl}}</td>
+ <td>{% if blog.approved  or user.is_superuser%}
+  <a href="blogposts/{{blog.id}}/">Posts</a><br/>
+  <a href="delete/{{blog.id}}/">Delete</a><br/>
+{%else%}
+Not approved yet.
+{%endif%}</td>
+{%if user.is_superuser %}
+<td>
+{%if blog.approved %}<a href="unapprove/{{blog.id}}/">Unapprove</a>{%else%}<a href="approve/{{blog.id}}/">Approve</a>{%endif%}<br/>
+{%if blog.userid %}<a href="detach/{{blog.id}}/">Detach from user</a><br/>{%endif%}
+  <a href="discover/{{blog.id}}/">Discover metadata</a><br/>
+{%if blog.blogurl %}<a href="undiscover/{{blog.id}}/">Undiscover metadata</a><br/>{%endif%}
+</td>
+{%endif%}
+</tr>
+{%endfor%}
+</table>
+{%else%}
+<p>We have no blogs registered to your account. To register
+a new blog or associate an existing one, please enter the
+URL to your RSS feed (PostgreSQL category only!) below.
+Note that if you attach an existing blog, it will temporarily
+be removed from planet while a moderator verifies that the
+attachment is correct.
+</p>
+<form method="post" action="new/">
+<input type="text" name="feedurl"><br/>
+<input type="submit" value="New blog">
+</form>
+{%endif%}
+{%if user.is_superuser %}
+<p>As superuser, you can add a new blog. Note that normally the user requests the addition and you
+then update it! Leave the userid blank to let the user attach it later, but that's a really ugly
+way to do it :-P</p>
+<form method="post" action="new/">
+<input type="text" name="feedurl"><br/>
+<input type="text" name="userid"><br/>
+<input type="submit" value="New blog">
+</form>
+{%endif%}
+{%endblock%}
diff --git a/planetadmin/register/templates/regbase.html b/planetadmin/register/templates/regbase.html
new file mode 100644 (file)
index 0000000..470ff98
--- /dev/null
@@ -0,0 +1,11 @@
+{%extends "base.tmpl" %}
+{%block content%}
+{%if user.is_authenticated %}
+<div style="float:right;"><a href="/register/logout">Log out</a></div>
+{%endif%}
+<h1>Welcome to planet administration</h1>
+{%if user.is_superuser %}
+<h2>You are registered as an administrator. BE CAREFUL!</h2>
+{% endif %}
+{%block regcontent%}{%endblock%}
+{%endblock%}
diff --git a/planetadmin/register/templates/registration/login.html b/planetadmin/register/templates/registration/login.html
new file mode 100644 (file)
index 0000000..e0f55e4
--- /dev/null
@@ -0,0 +1,25 @@
+{% extends "regbase.html" %}
+
+{% block regcontent %}
+
+  {% if form.errors %}
+    <p class="error">Sorry, that's not a valid username or password</p>
+  {% endif %}
+
+  <p>
+You need to log in to access your settings. This is done using a 
+PostgreSQL community account. If you do not have one, go to the main
+website and <a href="http://www.postgresql.org/community/signup">sign up</a>.
+  </p>
+  <form action='.' method='post'>
+    <label for="username">User name:</label>
+    <input type="text" name="username" value="" id="username">
+    <label for="password">Password:</label>
+    <input type="password" name="password" value="" id="password">
+
+    <input type="submit" value="login" />
+    <input type="hidden" name="next" value="{{ next|escape }}" />
+  <form action='.' method='post'>
+
+{% endblock %}
+
diff --git a/planetadmin/register/urls.py b/planetadmin/register/urls.py
new file mode 100644 (file)
index 0000000..fc61d2d
--- /dev/null
@@ -0,0 +1,26 @@
+from django.conf.urls.defaults import *
+from django.contrib.auth.views import login, logout, logout_then_login
+
+# Uncomment the next two lines to enable the admin:
+# from django.contrib import admin
+# admin.autodiscover()
+
+urlpatterns = patterns('',
+    (r'^$', 'planetadmin.register.views.root'),
+    (r'^new/$', 'planetadmin.register.views.new'),
+    (r'^approve/(\d+)/$', 'planetadmin.register.views.approve'),
+    (r'^unapprove/(\d+)/$', 'planetadmin.register.views.unapprove'),
+    (r'^discover/(\d+)/$', 'planetadmin.register.views.discover'),
+    (r'^undiscover/(\d+)/$', 'planetadmin.register.views.undiscover'),
+    (r'^detach/(\d+)/$', 'planetadmin.register.views.detach'),
+    (r'^delete/(\d+)/$', 'planetadmin.register.views.delete'),
+    (r'^modify/(\d+)/$', 'planetadmin.register.views.modify'),
+
+    (r'^blogposts/(\d+)/$', 'planetadmin.register.views.blogposts'),
+    (r'^blogposts/(\d+)/hide/(\d+)/$', 'planetadmin.register.views.blogpost_hide'),
+    (r'^blogposts/(\d+)/unhide/(\d+)/$', 'planetadmin.register.views.blogpost_unhide'),
+    (r'^blogposts/(\d+)/delete/(\d+)/$', 'planetadmin.register.views.blogpost_delete'),
+
+    (r'^login/$', login),
+    (r'^logout/$', logout_then_login, {'login_url':'/'}),
+)
diff --git a/planetadmin/register/views.py b/planetadmin/register/views.py
new file mode 100644 (file)
index 0000000..01ab2f0
--- /dev/null
@@ -0,0 +1,187 @@
+from django.http import HttpResponse, HttpResponseRedirect
+from django.template import RequestContext
+from django.shortcuts import render_to_response, get_object_or_404
+from django.contrib.auth.decorators import login_required, user_passes_test
+
+from planetadmin.register.models import *
+
+import socket
+import feedparser
+
+def issuperuser(user):
+       return user.is_authenticated() and user.is_superuser
+
+class pExcept(Exception):
+       pass
+
+@login_required
+def root(request):
+       if request.user.is_superuser:
+               blogs = Blog.objects.all()
+       else:
+               blogs = Blog.objects.filter(userid=request.user.username)
+       return render_to_response('index.html',{
+               'blogs': blogs,
+       }, context_instance=RequestContext(request))
+
+@login_required
+def new(request):
+       if not request.method== 'POST':
+               raise Exception('must be POST')
+       feedurl = request.POST['feedurl']
+       if not len(feedurl) > 1:
+               raise Exception('must include blog url!')
+
+       # See if we can find the blog already
+       try:
+               blog = Blog.objects.get(feedurl=feedurl)
+       except:
+               blog = None
+
+       if blog:
+               if blog.userid:
+                       return HttpResponse("Specified blog is already registered to account '%s'" % (blog.userid))
+               # Found a match, so we're going to register this blog
+               # For safety reasons, we're going to require approval before we do it as well :-P
+               blog.userid = request.user.username
+               blog.approved = False
+               blog.save()
+               return HttpResponse('The blog has been attached to your account. For security reasons, it has been disapproved until a moderator has approved this connection.')
+
+       if not feedurl.startswith('http://'):
+               return HttpResponse('Only http served blogs are accepted!')
+
+       # Attempting to register a new blog. First let's see that we can download it
+       socket.setdefaulttimeout(20)
+       try:
+               feed = feedparser.parse(feedurl)
+               status = feed.status
+               lnk = feed.feed.link
+               l = len(feed.entries)
+               if l < 1:
+                       return HttpResponse('Blog feed contains no entries.')
+       except Exception, e:
+               print e
+               return HttpResponse('Failed to download blog feed')
+       if not status == 200:
+               return HttpResponse('Attempt to download blog feed returned status %s.' % (status))
+       
+       blog = Blog()
+       blog.name = request.user.first_name
+       if request.user.is_superuser:
+               blog.userid = request.POST['userid']
+       else:
+               blog.userid= request.user.username
+       blog.feedurl = feedurl
+       blog.blogurl = lnk
+       blog.approved = False
+       blog.save()
+       return HttpResponseRedirect('..')
+
+@login_required
+def delete(request, id):
+       blog = get_object_or_404(Blog, id=id)
+       if not request.user.is_superuser:
+               if not blog.userid == request.user.username:
+                       return HttpResponse("You can only delete your own feeds! Don't try to hack!")
+       blog.delete()
+       return HttpResponseRedirect('../..')
+
+@user_passes_test(issuperuser)
+def modify(request, id):
+       blog = get_object_or_404(Blog, id=id)
+       blog.name = request.POST['blogname']
+       blog.save()
+       return HttpResponseRedirect('../..')
+       
+@user_passes_test(issuperuser)
+def approve(request, id):
+       blog = get_object_or_404(Blog, id=id)
+       blog.approved = True
+       blog.save()
+       return HttpResponseRedirect('../..')
+
+@user_passes_test(issuperuser)
+def unapprove(request, id):
+       blog = get_object_or_404(Blog, id=id)
+       blog.approved = False
+       blog.save()
+       return HttpResponseRedirect('../..')
+
+@user_passes_test(issuperuser)
+def discover(request, id):
+       blog = get_object_or_404(Blog, id=id)
+
+       # Attempt to run the discover
+       socket.setdefaulttimeout(20)
+       try:
+               feed = feedparser.parse(blog.feedurl)
+               if not blog.blogurl == feed.feed.link:
+                       blog.blogurl = feed.feed.link
+                       blog.save()
+       except Exception, e:
+               return HttpResponse('Failed to discover metadata: %s' % (e))
+
+       return HttpResponseRedirect('../..')
+
+@user_passes_test(issuperuser)
+def undiscover(request, id):
+       blog = get_object_or_404(Blog, id=id)
+       blog.blogurl = ''
+       blog.save()
+       return HttpResponseRedirect('../..')
+
+@user_passes_test(issuperuser)
+def detach(request, id):
+       blog = get_object_or_404(Blog, id=id)
+       blog.userid = None
+       blog.save()
+       return HttpResponseRedirect('../..')
+
+@login_required
+def blogposts(request, id):
+       blog = get_object_or_404(Blog, id=id)
+       if not blog.userid == request.user.username and not request.user.is_superuser:
+               return HttpResponse("You can't view/edit somebody elses blog!")
+       
+       posts = Post.objects.filter(feed=blog)
+
+       return render_to_response('blogposts.html',{
+               'posts': posts,
+       }, context_instance=RequestContext(request))
+
+def __getvalidblogpost(request, blogid, postid):
+       blog = get_object_or_404(Blog, id=blogid)
+       post = get_object_or_404(Post, id=postid)
+       if not blog.userid == request.user.username and not request.user.is_superuser:
+               raise pExcept("You can't view/edit somebody elses blog!")
+       if not post.feed.id == blog.id:
+               raise pExcept("Blog does not match post")
+       return post
+
+def __setposthide(request, blogid, postid, status):
+       try:
+               post = __getvalidblogpost(request, blogid, postid)
+       except pExcept, e:
+               return HttpResponse(e)
+       post.hidden = status
+       post.save()
+       return HttpResponseRedirect('../..')
+
+@login_required
+def blogpost_hide(request, blogid, postid):
+       return __setposthide(request, blogid, postid, True)
+
+@login_required
+def blogpost_unhide(request, blogid, postid):
+       return __setposthide(request, blogid, postid, False)
+
+@login_required
+def blogpost_delete(request, blogid, postid):
+       try:
+               post = __getvalidblogpost(request, blogid, postid)
+       except pExcept, e:
+               return HttpResponse(e)
+
+       post.delete()
+       return HttpResponseRedirect('../..')
diff --git a/planetadmin/settings.py b/planetadmin/settings.py
new file mode 100644 (file)
index 0000000..6b23002
--- /dev/null
@@ -0,0 +1,69 @@
+# Django settings for planetadmin project.
+
+DEBUG = False
+
+ADMINS = (
+   ('PostgreSQL Webmaster', 'webmaster@postgresql.org'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'postgresql_psycopg2'           # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+DATABASE_NAME = 'planetbeta'             # Or path to database file if using sqlite3.
+DATABASE_USER = 'planetadmin'             # Not used with sqlite3.
+DATABASE_PASSWORD = ''         # Not used with sqlite3.
+DATABASE_HOST = '/tmp'             # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
+
+TIME_ZONE = 'GMT'
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+USE_I18N = False
+
+MEDIA_ROOT = ''
+MEDIA_URL = ''
+ADMIN_MEDIA_PREFIX = '/media/'
+
+SECRET_KEY = '_q-piuw^kw^v1f%b6nrla+p%=&1bt#z%c$ujhioxe^!z%8q1l0'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+)
+
+ROOT_URLCONF = 'planetadmin.urls'
+
+TEMPLATE_DIRS = (
+    # Refer back to main planet templates
+    "../template",
+)
+
+INSTALLED_APPS = (
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'planetadmin.register',
+)
+
+AUTHENTICATION_BACKENDS = (
+    'planetadmin.auth.AuthBackend',
+)
+
+LOGIN_URL = '/register/login'
+
+# If there is a local_settings.py, let it override our settings
+try:
+       from local_settings import *
+except:
+       pass
+
diff --git a/planetadmin/urls.py b/planetadmin/urls.py
new file mode 100644 (file)
index 0000000..e6a8834
--- /dev/null
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import *
+
+# Uncomment the next two lines to enable the admin:
+# from django.contrib import admin
+# admin.autodiscover()
+
+urlpatterns = patterns('',
+    # Example:
+    (r'^register/', include('planetadmin.register.urls')),
+)
index dbaff93140cbcb38d0eef24a5bf5998f979c164c..29c873e3e4c69aabc8fbc68f3f9a93a56b410f2b 100644 (file)
@@ -6,7 +6,7 @@
   <meta http-equiv="Content-Type" content="text/xhtml; charset=utf-8" />
   <link rel="shortcut icon" href="/favicon.ico" />
   <link rel="alternate" type="application/rss+xml" title="Planet PostgreSQL" href="http://planet.postgresql.org/rss20.xml" />
-  <style type="text/css" media="screen" title="Normal Text">@import url("css/planet.css");</style>
+  <style type="text/css" media="screen" title="Normal Text">@import url("/css/planet.css");</style>
  </head>
  <body>
   <div id="planetWrap">