Commit 54df7e98 authored by Sorrel Harriet's avatar Sorrel Harriet

Lab 9 catflucks implementation with better error handling and tests!

parent ba431ec1
#!/usr/bin/env python3
"""
This module provides a set of reuseable functions
which provide the main app functionality.
We could even go a stage further and separate
database interactions from logic...
"""
# import modules from Python standard library
from bson.objectid import ObjectId
from datetime import datetime
import bcrypt
def login( db, form ):
""" Logs a user in if the Username and Password
submitted in a form match a record in database
Params:
db: handle to a database
form: FieldStorage object
Returns:
Dict
"""
# login status is false by default
status = False
try:
# get the username from the form
username = form['username'].value
# get the unhashed password sent in the form
pw = form['password'].value
except KeyError:
# a field must be missing from the form data
response = "I don't think you filled all the fields in..."
else:
# look for an account with the username
account = db.accounts.find_one({
"username":username,
})
# check if an account came back
if account is not None:
# compare the unhashed password from the form
# with the hashed version in the database
if bcrypt.checkpw( pw.encode('utf-8'), account['password'].encode('utf-8') ):
# if they match, output a personal greeting
# was this a DP student?
if account['username'] == "Sunday":
response = "What a good kitty. Now fill in the <a href='https://moduleevaluation.gold.ac.uk/surveyPrivacyStatement/Kf4iWgP74kSxYCxRA'>module feedback form</a> and you shall have some pie."
else:
response = "Hello {}!".format( account['name']['first'] )
status = True
else:
# if not, we know it was a wrong password
response = "Wrong password."
else:
# their username didn't match, so tell them that
response = "Sorry, I don't think I know you!"
# return a dict with the result and a message
return {"status":status,"msg":response}
def register_account( db, form ):
""" Registers a new user by inserting an account
in the accounts collection
Params:
db: handle to a database
form: FieldStorage object
Returns:
Dict
"""
# registration status false by default
status = False
try:
# get the username from the form
username = form['username'].value
# get the email from the form
email = form['email'].value
# get the unhashed password from the form
password = form['password'].value
except KeyError:
# a field must be missing from the form data
response = "I don't think you filled all the fields in..."
else:
# check if there is already an account with the email
# or username
result = db.accounts.find_one( { '$or': [
{ 'username' : username },
{ 'email': email }
]} )
# if so, send back a useful response
if result:
if email in result.values():
response = "Email address already registered."
else:
response = "Username taken."
# otherwise go ahead and make the account
else:
hashed_pw = bcrypt.hashpw( password.encode('utf-8'), bcrypt.gensalt() )
query = {
"username": username,
"password": hashed_pw.decode('utf-8'),
"is_admin": 0,
"created" : datetime.now().timestamp()
}
# make sure name fields aren't empty
# if they are, give them empty string values
if 'first' in form:
first = form['first'].value
else:
first = ""
if 'last' in form:
last = form['last'].value
else:
last = ""
# append name to query
query["name"] = { "first": first, "last": last }
# insert the account in the database
doc = db.accounts.insert( query )
if doc:
status = True
response = "New account registered. <a href='/cgi-bin/splash.py' title='login'>Click here</a> to go Log in!"
else:
response = "Problem registering your account. Please refresh the page to try again."
# return something useful...
return {'status': status, 'msg': response}
def latest_fluck( db ):
""" Returns details of the most recently flucked
cat in the database
Params:
db: handle to a database
Returns:
Dict
"""
# start with an empty dict
image = {}
# get details of last flucked cat
result = db.flucks.find({"is_flucked":1}).sort([('timestamp', -1)]).limit(1)
# get the associated image
for fluck in result:
result2 = db.images.find({"_id":fluck["image_id"]})
# if a doc was returned, put the bits we want in the image dict
for doc in result2:
image = {"url":doc["url"],"alt":doc["alt"]}
return image
def most_flucked( db ):
""" Returns details of the most flucked
cat in the database
Params:
db: handle to a database
Returns:
Dict
"""
# start with an empty dict
image = {}
# define a pipeline of operations to get most flucked image id
# from flucks collection
pipeline = [
{"$group": {"_id": "$image_id", "count": {"$sum": 1}}},
{"$sort": {"count":-1}},
{"$limit":1}
]
# apply pipeline of operations using aggregate
result = db.flucks.aggregate( pipeline )
# check if a result was returned
if result:
for fluck in result:
# get the associated image document
doc = db.images.find_one({"_id": fluck["_id"]})
# if a doc was returned, put the bits we want in the image dict
if doc:
image = {"url":doc["url"],"alt":doc["alt"]}
return image
def random_cat( db ):
""" Returns details of a random cat in the database
Params:
db: handle to a database
Returns:
Dict
"""
# start with an empty dict
image = {}
# get a random sample from images collection of size '1'
result = db.images.aggregate(
[{ '$sample': { 'size': 1 } }]
)
# if a result came back, do stuff with it...
if result:
# iterate through objects in the cursor (should only be 1)
for doc in result:
# put the bits we want in the image dict
image = {
'src': doc['url'],
'alt': doc['alt'],
'id': doc['_id']
}
return image
def count_flucks( db, img_id ):
""" Returns details of a random cat in the database
Params:
db: handle to a database
img_id: object id of an image document
Returns:
Dict
"""
# try to count flucks associated with image id
try:
num_flucks = db.flucks.find( {"image_id": img_id, "is_flucked":1} ).count()
# if img_id invalid, return false rather than crashing
except NameError:
return False
else:
return num_flucks
def insert_fluck( db, img_id, is_flucked ):
""" Inserts new fluck in flucks collection
based on form data
Params:
db: handle to a database
form: FieldStorage object
Returns:
Bool
"""
# try to insert a fluck
query = {
"image_id": ObjectId(img_id),
"is_flucked":is_flucked,
"timestamp":datetime.now().timestamp()
}
try:
result = db.flucks.insert( query )
# if img_id invalid, return something useful
except NameError:
return {'status': False, 'query': query}
# else return info about the inserted document
else:
return {'status': True, 'fluck_id': result}
"""
Define global config variables here.
These will be accessible from other
scripts, so we only need to set them
here.
"""
# app configurables stored in a config object
config = {
"SERVER_ADDRESS": "localhost",
"PORT": 27017,
"USER": "",
"PASS": "",
"DATABASE_NAME": "catflucks",
"TEMPLATE_DIR": "templates/"
}
#!/usr/bin/env python3
"""
This script handles the processing
of the register form in the catflucks application.
Note:
there is code repetition here...
What could we do about that?
Also note:
this functionality is not yet integrated
with the rest of the application.
How could it be?
"""
# import modules from Python standard library
import cgi
import cgitb
cgitb.enable()
import bcrypt
# import custom modules
from config import config
import utils
import components
# connect to database
db = utils.db_connect( config )
# tell the browser we are outputting HTML
print("Content-Type: text/html\n")
# render header HTML
print( utils.render_template( config['TEMPLATE_DIR'] + 'header.html') )
# get the form data
form = cgi.FieldStorage()
# check that register form was submitted
if 'btn_register' in form:
result = components.register_account( db, form )
msg = result['msg']
else:
msg = ""
# output the form as HTML
print( utils.render_template( config['TEMPLATE_DIR'] + 'register.html', data=[msg] ) )
# render footer
print( utils.render_template( config['TEMPLATE_DIR']+'footer.html' ) )
"""
This module contains some test functions that
can be called to check if a certain component
is doing what we expect it to do
"""
from config import config
import utils
import components
def test_connect_db( config ):
db = utils.db_connect( config )
print(db)
if db:
print("Connected to database. Trying to get a document...")
doc = db.flucks.find_one({})
if doc:
#print( doc )
print("PASS: db_connect")
else:
print("Error in db_connect. No handle returned.")
def test_insert_fluck( config ):
db = utils.db_connect( config )
result = components.insert_fluck( db, "5a15779bbc93104b641721d2", 1 )
if result['status']:
print("PASS: insert_fluck. Fluck {} inserted in database".format(result))
else:
print(result['query'])
print("FAIL: Error in insert_fluck")
# run tests...
test_connect_db(config)
test_insert_fluck(config)
#!/usr/bin/env python3
"""
This module provides a set of reuseable utility functions
This is a Google style docstring by the way.
Read more about them here:
https://www.python.org/dev/peps/pep-0257/
"""
from pymongo import MongoClient
def db_connect( config ):
""" Provides a connection to mongoDB database
Returns:
Object: A handle to a mongoDB database
"""
# try to create instance of MongoClient object
try:
client = MongoClient( config['SERVER_ADDRESS'], config['PORT'] )
except:
# raise a custom exception
raise Exception("Problem connecting to the database!")
# if we have a mongo client...
else:
# switch to the specified database
db = client[ config['DATABASE_NAME'] ]
# check a handle was returned
if db is not None:
# return a handle to the database
return db
def render_template( temp_path, data=[] ):
""" Reads in an HTML string from file
replacing any placeholders with values
supplied in data list.
Input params:
temp_path: the path to the template file
data: optional list of data values
Returns:
String: A formatted string of HTML
"""
try:
# open and read the template file
with open(temp_path, 'r') as f:
html = f.read()
except:
raise Exception("Could not open template")
else:
if data is not None:
try:
# replace placeholders with data in list
html = html.format(*data)
except:
raise Exception("Problem parsing data to template")
return html
body {
padding: 20px;
}
.feature-cat {
text-align: center;
float: right;
border: 2px solid #ccc;
width: 25%;
position: relative;
display: block;
margin-bottom: 20px;
clear: right;
}
.feature-cat h3 {
font-size: 16px;
}
.feature-cat img {
width: 100%;
}
.col-main {
padding: 10px;
border: 2px solid #ccc;
width: 70%;
float: left;
display: block;
margin-bottom: 20px;
}
.btn {
margin: 2px 0;
background-color: #666;
color: #fff;
cursor: pointer;
}
.btn-primary {
background-color: #563d7c;
}
.feature-cat.message {
position: absolute;
top: 100px;
left: 100px;
background: #eee;
width: 600px;
}
#!/usr/bin/env python3
# above 'she-bang' line makes the script executable from command line
""" A Simple Web Server
Run with ./simpleServer.py
Make sure all cgi scripts are executable
for single script:
chmod +x simpleServer.py
or for a whole directory:
chmod -r +x cgi-bin/
"""
import http.server # import http.server module
import cgitb; cgitb.enable() # import and enable cgitb module for exception handling
PORT = 8000 # specifies the port number to accept connections on
server = http.server.HTTPServer # provides simple web server
handler = http.server.CGIHTTPRequestHandler # provides request handler
server_address = ("", PORT) # specify server directory and port number
handler.cgi_directories = ["/","/cgi-bin","/htbin"] # where CGI scripts will reside in relation to the `server' directory
print("Starting server...") # outputs a message
httpd = server(server_address, handler) # creates the server, passing it the server address and port number, as well as the CGI handler (httpd stands for HTTP Daemon)
print("serving at port", PORT) # outputs a message
httpd.serve_forever() # puts program in infinite loop so that the server can `serve_forever'
<div class="col-main">
<h3>Vital Catistics</h3>
<p>This poor cat has been flucked {} times already.</p>
</div>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script>
</body></html>
<div class="col-main">
<form method="POST" action="{}">
<div class="form-group">
<input type="hidden" value="{}" name="img_id">
<input class="form-control btn" name="btn_skip" type="submit" value="Skip">
<input class="form-control btn btn-primary" name="btn_fluck" type="submit" value="Fluck">
</div>
</form>
</div>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" type="text/css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<title>Catflucks</title>
</head>
<body>
<h1>Welcome to Catflucks</h1>
<div class="feature-cat">
<h3>Recently flucked</h3>
<!-- present most recently flucked cat -->
<img src="{}" alt="{}" width=150>
</div>
<div class="col-main">
<h3>Log in</h3>
<form method="POST" action="/cgi-bin/splash.py">
<div class="form-group">
<label for="username">Username:</label>
<input class="form-control" type="text" name="username">
<label for="password">Password:</label>
<input class="form-control" type="password" name="password">
<input class="form-control btn btn-primary" type="submit" name="btn_login" value="Enter">
</div>
</form>
<div class="msg"><p>{}</p></div>
<div><a href="/cgi-bin/register.py">Register</a></div>
</div>
<div class="feature-cat">
<h3>Most flucked</h3>
<!-- present the most often flucked cat -->
<img src="{}" alt="{}" width=150>
</div>
<!-- THINK:
- What is unusual about this register form?
- What are the implications of that?
- How might you help a user who has forgotten their password?
- FEEL FREE TO IMPROVE THIS FORM!
-->
<div>
<h3>Register</h3>
<form method="POST" action="/cgi-bin/register.py">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" name="username" required>
<label for="password">Password:</label>
<input type="password" name="password" required>
<label for="email">Email:</label>
<input type="email" name="email" required>
</div>
<div class="form-group">
<label for="first">First name:</label>
<input type="text" name="first" placeholder="First name">
<label for="last">Last name:</label>
<input type="text" name="last" placeholder="Last name">
</div>
<div class="form-group">
<input type="submit" name="btn_register">
</div>
</form>
<div class="msg"><p>{}</p></div>
</div>
<div class="col-main">
<p>You are viewing a random image of a cat.</p>
<img src="{}" alt="{}" width=500>
</div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment