Created
February 28, 2012 07:58
-
-
Save glamrock/1930461 to your computer and use it in GitHub Desktop.
John James's Affinity99.net script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
Affinity demo, Ver. 0.1. Open source, for Python3.1 using Bottle. 2012-02-14 | |
Copyright 2011, 2012 by John S. James | |
Licensed under the Apache License, Version 2.0 (the "License"); | |
you may not use this file except in compliance with the License. | |
You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License. | |
''' | |
import sqlite3 | |
import sys | |
from bottle import route, run, debug, template, SimpleTemplate, request | |
from bottle import response, validate, static_file, error, get, post | |
from random import randrange | |
from hashlib import sha256 | |
from time import time, localtime, sleep, gmtime, strftime | |
global accts | |
global aff_key | |
global phone_flag # True for better display on some phones | |
global international_flag # '/i' if international version (using Google Translate) | |
international_flag = '' | |
# Define the following constants here to get them in the outer scope: | |
db_name = 'demo.sqlite3' # Name of the Sqlite database -- used write-only, | |
# except when starting or reloading the server | |
accts = {} # Dictionary which associates hashed account name with that account's data | |
instruction_line = '''Make changes, press <b>Publish</b> below.''' | |
# To abandon changes, close window. Note: <b><b>bold</b></b>, <h2>, & links OK.<p> | |
################################################################################ | |
# SQL database section ######################################################### | |
################################################################################ | |
def sql_init(): | |
''' Initialize dictionary from the SQL database (usually only after server reload). ''' | |
global accts | |
global international_flag # '/i' if international version | |
# Special -- so next two will be initialized, and saved between user requests | |
global old_key | |
global old_alias | |
old_key = '' | |
old_alias = '' | |
db = sqlite3.connect(db_name) | |
c = db.cursor() | |
c.execute('''PRAGMA secure_delete = ON;''') # Test this #***** | |
c.execute('''PRAGMA secure_delete;''') | |
c.execute('''CREATE TABLE IF NOT EXISTS accts_table (key TEXT PRIMARY | |
KEY, r_type TEXT, ctime TEXT, mtime TEXT, color TEXT, link | |
TEST, test TEXT);''') | |
c.execute("select * from accts_table") | |
for row in c: | |
(xkey, r_type, ctime, mtime, color, link, test) = row | |
data = (r_type, ctime, mtime, color, link, test) | |
accts[xkey] = data | |
db.commit() | |
db.close() | |
return | |
def sql_save(xkey, xdata): | |
''' Save either a new or updated account in the accts dictionary. ''' | |
global accts | |
global international_flag # '/i' if international version | |
(r_type, ctime, mtime, color, link, test) = xdata | |
mtime = time() | |
db = sqlite3.connect(db_name) | |
c = db.cursor() | |
c.execute('''INSERT OR REPLACE INTO accts_table (key, r_type, ctime, | |
mtime, color, link, test) VALUES (?, ?, ?, ?, ?, ?, ?)''', | |
(xkey, r_type, ctime, mtime, color, link, test) ) | |
db.commit() | |
print('++++++++ N=', len(accts), ':', xkey[0:7], r_type, ctime, mtime, color, link[0:5], test[0:250]) | |
db.close() | |
return | |
def sql_delete(xkey): | |
''' Delete a record from the accounts dictionary. ''' | |
global accts | |
global international_flag # '/i' if international version | |
db = sqlite3.connect(db_name) | |
c = db.cursor() | |
c.execute('''DELETE FROM accts_table WHERE key = '%s';''' %xkey) | |
db.commit() | |
db.close() | |
return | |
def sql_dump(): | |
''' Dump the whole database, debug only. ''' | |
global accts | |
global international_flag # '/i' if international version | |
print('-------------------------------------------------') | |
db = sqlite3.connect(db_name) | |
c = db.cursor() | |
c.execute('''SELECT * FROM accts_table''') | |
for record in c: | |
s = "{}, {}, {}, {}, {}, {}, {}".format( | |
record[0], record[1], record[2], record[3], | |
record[4], record[5], record[6] ) | |
print(s) | |
db.close() | |
return | |
################################################################################ | |
# Main program: handle query-type string ####################################### | |
################################################################################ | |
def do_query(query_string): #*** | |
''' Use the query string to look up a text in the accounts dictionary.''' | |
print ('yyyy', query_string) | |
z = css_page + form_publish_tpl.render(place_affinity_text_members = | |
affinity_text_members) # Return the right thing here (no GO) | |
return(z) | |
################################################################################ | |
# Main program: Affinity ####################################################### | |
################################################################################ | |
def aff_init(): | |
''' Initialize for Affinity ''' | |
global aff_key | |
global phone_flag | |
print() | |
print('Affinity initialization start', time()) | |
phone_flag = False; | |
aff_name = "aff_test" | |
b_aff_name = bytes(aff_name, 'utf-8') | |
aff_key = sha256(b_aff_name).hexdigest() | |
# In the beginning (or after a crash or other server start or reload), | |
# read the accounts from disk into memory: | |
if len(accts) == 0: | |
print('must initialize accts dictionary from the disk') | |
sql_init() | |
if aff_key not in accts: # Must initialize this record | |
print('must initialize affinity main record') | |
form_text = '' | |
r_type = 'aff' | |
ctime = time() | |
mtime = time() | |
color = '' | |
link = '' | |
test = form_text | |
data = (r_type, ctime, mtime, color, link, test) | |
accts[aff_key] = data | |
sql_save(aff_key, data) | |
print('Affinity initialization end', time()) | |
return | |
################################################################################ | |
# Main program: routines reachable by @route ################################### | |
################################################################################ | |
@route('/favicon.ico', method="GET") | |
def tf(): | |
print('favicon') | |
return | |
@route('/', method="GET") | |
@route('/demo123', method="GET") | |
@route('/demo123/', method="GET") | |
def aff_print_form(): | |
''' Initial print of the project page (and an 'Edit' button)''' | |
global aff_key | |
ip = request.environ.get('REMOTE_ADDR') #*** | |
# or ip = request.get('REMOTE_ADDR') | |
# or ip = request['REMOTE_ADDR'] | |
print('ip', ip) | |
for key in request: | |
if key != 'bottle.request': | |
print (key, request[key]) | |
aff_init() | |
data = accts[aff_key] | |
(r_type, ctime, mtime, color, link, test) = data | |
z = '' | |
for line in test.splitlines(): | |
z = z + line + '<br>' # escape(line).replace(' ', ' ') + '<br>' | |
return [css_page + form_edit_button + z + '<br>' + form_edit_button + '</div></html>'] | |
@route('/', method="POST") | |
@route('/demo123', method="POST") | |
@route('/demo123/', method="POST") | |
def aff_edit(): | |
''' Handle the buttons.''' | |
global aff_key | |
print ('aff_edit POST') | |
name = sorted(request.POST)[0] # 'name' tells which form came in | |
print('name= ', name) | |
if name == 'Edit': # 'Edit' button was pressed pressed | |
data = accts[aff_key] | |
(r_type, ctime, mtime, color, link, test) = data | |
r_type = 'aff' | |
ctime = time() | |
mtime = time() | |
color = '' | |
link = '' | |
data = (r_type, ctime, mtime, color, link, test) | |
accts[aff_key] = data # Later, add a collision test | |
print('dictionary ', len(accts), ' items') | |
sql_save(aff_key, data) | |
nlines = 0 # test.count('\n') | |
lines = test.splitlines() | |
for line in lines: | |
nlines = nlines + int(len(line)/72) | |
nlines = int(nlines * 1.05) # Add 5% to overcompensate for word wrap | |
return [css_page + instruction_line + form_publish_tpl.render(place_form=test, place_nrows=nlines)] | |
elif name=='txt': # 'Publish' button was pressed | |
test = request.POST.get('txt').strip() | |
print('XXX', request) #*** | |
r_type = 'aff' | |
ctime = time() | |
mtime = time() | |
color = '' | |
link = '' | |
data = (r_type, ctime, mtime, color, link, test) | |
accts[aff_key] = data # Later, add a collision test | |
print('dictionary2 ', len(accts), ' items') | |
sql_save(aff_key, data) | |
z = '' | |
for line in test.splitlines(): | |
z = z + line + '<br>' | |
return [css_page + form_edit_button + z + '<br>' + form_edit_button + '</div></html>'] | |
elif name=='source': # 'Print source code and license' button was pressed | |
z = '' | |
source = open('aff.py', mode = 'r') | |
for line in source: | |
z = z + escape(line).replace(' ', ' ') + '<br>' | |
return [css_page + z + '</div></html>'] | |
else: | |
return 'error??????????????' | |
################################################################################ | |
# Forms ######################################################################## | |
################################################################################ | |
# {{place_nrows}} | |
form_publish_aff_page = ''' | |
<div style="padding: 2px 1% 2px 1%; color: black; background-color: #ffffff; font-weight:normal;"> | |
<form action="" method="POST"> | |
<textarea name="txt" cols="35" rows="15" style="margin:0; padding:0; border: 2px solid #18ff18;"> | |
{{place_form}} | |
</textarea><br> | |
<input type="submit" value="Publish" class="button"> | |
</form> | |
</div> | |
<form method="Post" action=""><input type="submit" name="source" class="button" value="Print source code and license"></form> | |
''' | |
form_edit_button = ''' | |
<div> | |
<form action="" method="POST"> | |
<input type="submit" name="Edit" value="Edit (click to make changes)"> | |
</form> | |
</div> | |
''' | |
################################################################################ | |
# Main program: Miscellaneous ################################################## | |
################################################################################ | |
def hex_validate (char): | |
''' Change 'o' and 'O' to '0', then anything except 0-9, a-f, and A-F, to f ''' | |
if char in 'oO': | |
char = '0' # Set O or o to 0 | |
if char not in 'abcdefABCDEF0123456789': | |
char = 'f' | |
return char | |
def escape(s): | |
''' Escape user-entered data to prevent malware; also, escape HTML for clean | |
listing. ''' | |
s = s.replace('&', '&') # Do this one first | |
s = s.replace('<', '<') | |
s = s.replace('>', '>') | |
s = s.replace('"', '"') | |
s = s.replace('/', '/') | |
s = s.replace("'", ''') # avoid ' | |
return s | |
def unescape(s): | |
''' Reverse the action of 'escape', above. ''' | |
s = s.replace('<', '<') | |
s = s.replace('>', '>') | |
s = s.replace('"', '"') | |
s = s.replace(''', "'") | |
s = s.replace('/', '/') | |
s = s.replace('&', '&') # Do this one last | |
return s | |
################################################################################ | |
# Templates, documentation ##################################################### | |
################################################################################ | |
css_page = ''' | |
<!DOCtype HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" | |
"http://www.w3.org/TR/html4/loose.dtd"> | |
<html> | |
<head> | |
<meta http-equiv="Content-type" content="text/html;charset=utf-8" > | |
<title>RepliCounts: New ways to pay artists online</title> | |
<link rel="canonical" HREF="http://www.replicounts.com/"> | |
<link rel="icon" HREF="http://stat.replicounts.com/imagexx/favicon.ico" | |
type="image/x-icon" > | |
<style type="text/css"> | |
html { | |
color: #282828; | |
background-color: #ffffff; | |
font-size: 1em; | |
font-family: helvetica, arial, sans-serif; | |
max-width: 1500px; | |
min-width: 200px; | |
min-height: 160px; | |
margin: 0; | |
padding: 5px 2% 2px 2%; | |
} | |
body { margin: 0;} | |
form {margin: 0;} /* fix for MSIE 6, CR ??? */ | |
textarea { | |
display: block; | |
width: 99%; | |
background: #ffffff; | |
border: 1px solid #18c018; | |
font: 1em Verdana, sans-serif; | |
overflow: auto; | |
} | |
hr { border: 1px solid #ff1818; width: 200px; text-align:left; margin: .5em 0 1.12em 0; | |
color: #ff1818; background-color: #ff1818; height: 2px;} | |
b {color: #ff1818;} | |
big {font-size: 1.17em;} | |
small {font-size: .83em;} | |
h1 { font-size: 1.5em; margin: 10px 0 0 0; text-align: left;} | |
h2 { font-size: 1.17em; margin: 0; text-align: left;} | |
h3 { font-size: 1em; margin: 0; text-align: left;} | |
blockquote, ul, | |
fieldset, form, | |
ol, dl, dir, | |
h1, h2, h3, h4, b, strong { font-weight: bold } | |
ul, ol { list-style-position: inside; } | |
li { margin: 0; padding: 0;} /** try padding:0 for MSIE 8.0 etc.?? */ | |
ol { list-style-type: decimal } | |
a:link { color:#1122cc; font-weight:bold; } | |
a:visited { color: purple; } | |
a:hover { background-color:#ffff00; } | |
.button {background-color: #99ccff; color:red; font-weight:bold; font-size:1em; margin:5px; } | |
.button:hover{background-color:#ffff00;} | |
</style> | |
<style type="text/css" media="print"> | |
body{ margin: 05px } | |
</style> | |
''' | |
affinity_doc_html = ''' | |
<h1>Affinity:</h1> | |
<h2>Online coordination for small groups [not ready yet]</h2> | |
"Affinity" creates a <b>project page</b> for private but not-very-secret online coordination of a small task-force or team. The group members must be "on the same page" (able to trust each other) -- hence the name Affinity (suggested by the affinity groups of recent activist movements). We designed Affinity for small groups, such a 5 or 6 people, and usually no more than 15 or so, though there is no upper limit. And you can also use Affinity yourself, as an online to-do list or other online memo pad. | |
<p> | |
The major advantages of the Affinity software are:<ul><li>Affinity keeps the project information <b>organized in one place</b> (unlike email).</li> | |
<li>Affinity requires <b>no training at all</b> for most group members (they already know how to click), very little training for the administrator who starts a new group, and even less for group members who update the project page. And <b>no one needs to have an email address</b>.</li> | |
<li>Affinity should work on <b>almost any phone, computer</b>, or other device that can reach the Web -- and work the same on all of them. You don't need to wait for someone to write an app for your equipment.</li> | |
<li>It can be effective even over very <b>poor Internet connections</b> -- so bad that reading online email or doing a Google search would be difficult. (We've found it the easiest and best way to quickly test if there is any Internet access at all.)</li> | |
<li>The Web forms can be any reasonable length. (But we plan to limit each Web form to 15,000 characters [about 2,000 words], for this software demonstration project. If that becomes a problem, let us know.)</li> | |
<li>Affinity is free, ad-free, analytics-free (to reduce delay), and open-source (under the Apache 2.0 license -- so you can copy the software, make changes if you want, and run your own version on your server).</li> | |
<li>It <b>downloads no software</b> to run on your computer (unless you request Google Translate, which does download JavaScript) -- reducing the danger of malware. And you never need to do anything to upgrade or maintain Affinity.</li> | |
<li>If you lose access to or control of your project page, you can recover with little trouble. In the worst case, just start a new project page, copy the contents of each Web form in the old one (or from a saved copy) to the corresponding form in the new one, and distribute the new members' link to the group. Then you can either delete or just abandon the old project page.</li> | |
<li>Affinity can work in <b>almost any written language on Earth</b> (since it uses Unicode throughout).</li> | |
</ul> | |
<p> | |
Affinity maintains a single project page online (for each small-group project). The page can be a to-do list, a calendar, discussions, a list of important articles or other Web links, a FAQ (frequently asked questions) that's very easy to use and to update, anything else, or any or all of the above (on the same project page, or on different ones). | |
<p> | |
The project page consists entirely of Web forms. We did this because almost everybody already has experience updating Web forms (if only to give information to corporate or government bureaucracies), so they should not need to learn a new editor in order to write to or change the Affinity project page. | |
<p> | |
Any group member can change or delete anything someone else has written on the main project page. But only the administrator can change two other project pages, before and after the main page. Anyone who has the link to the project page can read all three pages, however. | |
<p> | |
There are <b>no passwords, no registration, no logging in</b>. Instead, the administrator who creates a new group can provide names for two Web links, usually names difficult or impossible to guess. One of these links is for all group members. The other is just for the administrator -- the person who sets up the group, plus others (if any) who are given the administrative link. | |
<p> | |
So you can think of Affinity as a shared writing pad (the "project page" that the group has in common) -- or as a much-simplified wiki. Unlike a standard wiki, Affinity keeps <b>no history of changes</b>; instead, anybody can change anything (unless the particular Web form it's in has been protected by an administrator, in which case only an administrator can change it). | |
<p> | |
Once something is deleted it's gone -- which is why the group must be small enough to trust each other and be able to work together. Any group member (anyone who has the link given to group members) can read the whole project page, and make changes on part of it; but if the user's link does go astray and get to the wrong person(s), such as a hostile saboteur, an administrator can cancel it and replace it with another link, then distribute the new link to the legitimate group members. | |
<p> | |
Note that search engines will not even see an Affinity page (unless someone publishes the link to it, which for most Affinity groups will be a no-no). If you do want to have a public, searchable project page, then Affinity could do it (just publish the members' link where search engines will see it). | |
<p> | |
Users and administrators can of course bookmark ("favorite") their link to the project page, for convenience. Then one click will show them the entire current project page. | |
<p> | |
What this means is that any group member, anywhere, can download the project page usually in a few seconds or less, with any phone, pad, or other computer that's handy, with no software to download or other advance preparation -- even over a poor Internet connection. For example, someone can immediately download the project page they are working with, including the current status of the work and mission -- before the train they are riding goes underground or otherwise out of phone/Internet access. They can still read the page while cut off from the Internet, make plans, and make notes elsewhere about updates they want to post on the page in later, when Internet access is back. | |
<h3>Temporary Limitations:</h3> | |
The first version will have some problems that will be corrected later. | |
<ul> | |
<li>If two group members are changing the project page at the same time (very unlikely in a small group, since generally about 90% of users only read the online information and do not add to it or change it), then the changes made by the person who writes first will be lost, and need to be entered again. We will fix this by automatically reserving write exclusivity for a short time, perhaps 15 minuters.</li> | |
<li>Web links may not be active at first (meaning that group members can still share them, but will need to copy and paste them into a browser's address bar). | |
<li>The first version of Affinity may only have two Web forms on a project page -- one that only administrators can change, the other allowing changes by all group members. (All group members can always read all of the project page.)</li> | |
<li>To control malware, we will not allow full use of HTML on a project page, but will inactivate all except emphasis and headings (and we will automatically activate Web links). We will add more HTML if we can do so safely, and users want it. But our focus is simplicity, so we are reluctant to complicate the Affinity online-coordination system. | |
</ul> | |
<h2>How to Start an Affinity Page</h2> | |
(to be continued) | |
''' | |
form_publish_tpl = SimpleTemplate(form_publish_aff_page) # For query string only? | |
sys.stdout = sys.stderr | |
debug(True) # Comment this out later, for production server | |
################################################################################ | |
# Basic tests ################################################################## | |
################################################################################ | |
# Do these final tests on the remote server. Debug should be on. | |
# Add the main tests here!!! | |
# Test each page with and without the final slash. | |
# Make sure no errors were printed in the terminal window -- while doing the above in debug mode. | |
# Make sure the test accounts above still exist, after stopping and restarting rep.py at the server. | |
# Test all buttons in the system | |
# Test all error messages | |
# Validate with W3C html, css checks -- for "view source" from each page. | |
# Spell check. | |
# Test the printing | |
# Try entering garbage and/or malware at all user inputs. | |
# Disable CSS (on a test copy), to make sure the system is still usable. | |
# Later: Test that the PRAGMA works; Do 301 if a visitor doesn't use www; Change arbitrary case after .com to lower (handle this in the 404?); check, does MSIE 8.0 etc. now avoid extra spaces in list?; Make <hr> show up on printouts; | |
# Deployment: | |
#It should be easy to run this file on another server unchanged -- since the only change needed to move from the local test server to the remote server is to comment out the 'localhost' line below, change the following line for your own remote server, and everything else runs identically. Of course you must change the port, and make other changes required by your host (which must be able to run Python ver. 3 -- not ver. 2). You need to have a recent bottle.py file in the same directory as this file, rep.py; that should be all you need to do to install Bottle, the "microframework" we used instead of the much larger Django (which was not ready for Python version 3 -- and which this project does not need anyway).''' | |
# Technical note: To get a machine-readable copy of the source code (which includes the documentation for Affinity), select and copy the text from your browser screen into a text file. Otherwise you may get unwanted escape codes for several characters inserted into the file. We used these codes to produce a clean listing, and all browsers will recognize the codes and remove them. Do not copy from "View Source," or the codes will be back. | |
# Comment out the next line when uploading for the remote server. Also, turn debug off for production. | |
#run(host='localhost', port=8080, reloader=True) | |
run(port=28606) | |
# affmain 28606; aff 17065; repc 51874 | |
#You can use your browser's BACK button to return from the source-code listing |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment