Skip to content
Snippets Groups Projects
Commit 2ea1a24b authored by Woojong Nam's avatar Woojong Nam
Browse files

init project

parent 3c404ab8
No related merge requests found
Pipeline #900 failed with stages
Showing
with 649 additions and 1 deletion
# catflucks-project # Lab 9: Template README
Here is a template README file you could include in your final app submission, so that your marker knows exactly where to look for evidence of your learning.
Doing this is another way of showing that you understand the code you wrote!
**Note:** They will also be looking at your commit history, to see how much of the app you wrote from scratch.
## Evidence
| Checklist point | Evidence (filename(s) and line number(s) and/or short desciption) |
|-------------|------------|
| write a simple server script which is capable of serving a web application written in Python | |
| retrieve one or more documents or rows from one or more collections or tables | |
| iterate over documents returned in a results cursor object | |
| filter and/or sort documents in the result set based on some simple criteria | |
| perform more advanced filtering and/or aggregation operations in a database query | |
| handle a POST request made via an HTML form in a server-side script | |
| demonstrate consideration for Separation of Concerns through the modularisation (separation) of related code | |
| demonstrate an awareness of how related data is modelled in the database | |
| design and implement an original functional feature in a Python web app | |
| other relevant extension of the taught material (if applicable) | |
| make a Python script self-executable | |
| utilise a range of Python's built-in functions and methods | |
| make use of user-defined functions | |
| design and implement reuseable functions | |
| write readable, well-presented code | Everywhere?! |
## Installation instructions
If you have used any modules or configured any global variables that a tester of your app should be aware of, please detail them here.
For example, in the case of this example app:
+ The app requires Python 3+ and MongoDB 3.2 or later
+ The bcrypt and pymongo modules should be are available from your python environment
+ You should open and set the variables in cgi-bin/config.py to match your configuration
+ To launch,
./simpleServer.py
+ Restore the database by running `mongorestore` from the base directory
+ After restoring the database, you can test the app with these login credentials:
- Username: tester
- Password: password
File added
File added
File added
#!/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' ) )
#!/usr/bin/env python3
# import modules from Python Standard library
import cgi
import cgitb
cgitb.enable()
# import custom modules
from config import config
import utils
import components
# connect to a database
db = utils.db_connect( config )
# tell browser to expect HTML
print("Content-Type: text/html\n")
# render header HTML
print( utils.render_template( config['TEMPLATE_DIR'] + 'header.html') )
# get any data sent with the GET or POST request
# this may be required by multiple components
sent_data = cgi.FieldStorage()
# -------- START OF FUNCTIONAL COMPONENTS ----------->>>
# ---------- HANDLE LOGIN FORM SUBMISSIONS ----------
# check if login form was submitted
if 'btn_login' in sent_data:
# it was, so call the login function
result = components.login(db, sent_data)
msg = result['msg']
else:
# message displayed in login form will be empty
result = components.login(db, sent_data)
msg = ""
if result['status'] == False:
print( utils.render_template( config['TEMPLATE_DIR'] + 'login.html', data=[msg] ) )
elif result['status'] == True:
print(
"""
<div>Hello {}</div>
""".format()
)
# ---------- HANDLE FLUCK FORM SUBMISSIONS -----------
# check if fluck occurred
if 'btn_fluck' in sent_data:
# user flucked, so is_flucked is 1
result = components.insert_fluck(db, sent_data['img_id'].value, 1)
# check if skip occurred
elif 'btn_skip' in sent_data:
# user skipped, so is_flucked is 0
result = components.insert_fluck(db, sent_data['img_id'].value, 0)
# ------ GET LATEST FLUCKED CAT -------
# call latest_fluck to get image details
image = components.latest_fluck( db )
# check image isn't empty before proceeding...
if image:
# it isn't so render the template
print( utils.render_template( config['TEMPLATE_DIR']+'latest_fluck.html', data=[image["url"],image["alt"]] ) )
# ------ GET MOST FLUCKED CAT -------
# call latest_fluck to get image details
image = components.most_flucked( db )
# check image isn't empty before proceeding...
if image:
# it isn't so render the template
print( utils.render_template( config['TEMPLATE_DIR']+'most_flucked.html', data=[image["url"],image["alt"]] ) )
# ------ SERVE UP A RANDOM CAT WITH STATS ------
# get one random document from images collection
current_cat = components.random_cat( db )
if current_cat:
# render serve_cat template, passing it dynamic data
print( utils.render_template( config['TEMPLATE_DIR']+'serve_cat.html', data=[current_cat['src'], current_cat['alt']] ) )
# count flucks of current cat
num_flucks = components.count_flucks( db, current_cat['id'] )
# check num_flucks exists
if num_flucks:
# render cat_stats template, passing it dynamic data
print( utils.render_template( config['TEMPLATE_DIR']+'cat_stats.html', data=[num_flucks] ) )
# ------- OUTPUT FLUCK/SKIP FORM ----------
if current_cat:
# render form_fluck template, passing it dynamic data
print( utils.render_template( config['TEMPLATE_DIR']+'form_fluck.html', data=["/cgi-bin/splash.py", current_cat['id']] ) )
# --------- /END OF COMPONENTS --------
# 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;
}
File added
{"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"catflucks.accounts"}]}
\ No newline at end of file
File added
{"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"catflucks.flucks"}]}
\ No newline at end of file
File added
{"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"catflucks.images"}]}
\ No newline at end of file
#!/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>
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