Image created and taken from Am I Responsive?.
This is a fictional website (for educational purposes) for a street food company named Get Wurst. They sell Bratwursts, Burgers and Chicken. The site can be used to view the menu, order and pay for items and view a sample of previous events the company has worked at. A Frequently Asked Questions page allows users to view previous questions and answers. If the user is logged in they can submit questions, answer previous questions and like answers.
A kanban was used to keep an agile view on user stories. This can be seen here. User stories were added, and labels for bugs or to change where necessary.
Three coloumns were used (To Do, In Progress and Done) and issues/user stories were moved between the colomns as progress was made. Additional information was added throughout the process in an agile manner.
The following table was used to plan the development of the project. This helped to easily break down Themes into Epics, their related User Stories and prioritise their importance. This will be updated as development progresses.
Theme | Epic | Related User Stories | MOSCOW | Completed |
---|---|---|---|---|
Account Management | Sign up | #15 Sign up using email, username and password | Must have | Yes |
#16 Sign up using Google | Should have | Yes | ||
#17 Confirm Sign up via email | Should have | Yes | ||
Sign in | #18 Log in using username / password | Must have | Yes | |
#19 Login using Google | Should have | Yes | ||
#20 Forgot sign in info | Should have | Yes | ||
#21 Login state | Should have | Yes | ||
Log out | #22 Manual log out | Must have | Yes | |
#23 Automatic log out | Won't have | No | ||
Products | Add products | #24 Add products through admin | Must have | Yes |
#20 Add products through site | Must have | Yes | ||
View products | #25 View all products | Must have | Yes | |
#26 View products by category | Must have | Yes | ||
View new products | Won't have | No | ||
#26 Search products | Should have | Yes | ||
Edit products | #27 Edit products through site | Must have | Yes | |
#37 Edit products through Django admin | Must have | Yes | ||
Delete products | #38 Delete products through admin | Must have | Yes | |
#28 Delete products through site | Must have | Yes | ||
Cart | Make purchase | #29 Add item to cart | Must have | Yes |
#30 Change product quantity in cart | Should have | Yes | ||
#31 Remove items from cart | Should have | Yes | ||
#32 See order total | Must have | Yes | ||
#33 Pay for goods | Must have | Yes | ||
#34 Know if an order was successful | Must have | Yes | ||
#35 Email order confirmation | Should have | Yes | ||
Profile | Details | #40 Save profile details | Should have | Yes |
#41 Edit profile details | Should have | Yes | ||
#42 View order history | Should have | Yes | ||
FAQ | #43 Submit a question | Must have | Yes | |
#44 Submit an answer | Must have | Yes | ||
#46 Email when question answered | Could have | Yes | ||
Events | #45 See previous events | Should have | Yes | |
Marketing | #36 Add Facebook page | Must have | Yes |
When a site owner is logged in the menu page has additional "Edit" and "Delete" buttons for each product.
A logged in user can submit a question, an answer and like/remove their like of published answers. Users are told if a question doesn't have a published answer.
Non-logged in users can view published questions and answers and see how many people have liked the answer.
Images created and taken from Balsamiq
An ERD for the site can be found below. This was created using Django-extensions.
Typespiration was used to find complimenting Google Font pairings, with the following decided upon:
Once the hero image was chosen a shade of yellow from the cheese was used for the sites primary colour. A secondary, lighter colour was chosen for the secondary colour. These were defined in the css as variables:
Primary colour: #EFC102
Primary lighter colour: #F0E582
The following colours were used as variables for primary buttons:
Primary colour: #9DD9D2
Primary dark colour: #71ABA5
Primary darker colour: #46807A
Primary darkest colour: #1A5751
The following colours were used as variables for the call to action buttons:
CTA primary colour: #FFB8E4
CTA dark colour: #CF8BB6
CTA darkest colour: #A1618A
The following colours were used as variables for links:
Primary colour: #25598E
Hover colour: #1D426D
Visited colour: #153251
The navigation bar allows the user to easily select which area of the site they wish to view. It will be located at the top of the site, as this is common practice and is the area where most users eyes will be initially drawn. When a link is hovered over, it becomes highlighted to help the user confirm they are about to select the relevant link. The highlight remains once it is selected.
The 'hamburger' icon was used on large screens, where the navigation bar will collapse and be represented as three horizontal lines. When touched or clicked, this displays the navigation in a dropdown menu. This is done due to the limited real estate space on the screen, which makes the text hard to read at this size.
When a user is not logged in, the right side of the navigation includes "Sign Up" and "Login" links. A logged in user is shown a "My Profile" and "Logout" link and staff members have an additional "Product Management" dropdown link.
When the "FAQ's" link is hovered over, its non-abbreviated version, "Frequently Asked Questions," is displayed. This helps usability and can also help with translation systems and SEO.
A secondary navigation bar below has links to the product categories: bratwurst, Bratwurst, Chicken and Burgers. The search bar within this navigation can be used to search for words within a product's title, description and short description.
A user can login using their Google account or their Get Wurst username and password. The user can check the "Remember Me" checkbox if they want to be automatically logged in the next time they use the site.
They can also click a link if they have forgotten their password.
A user can sign up to the site by entering an email, username and password. Email and passwords fields need to be entered twice and are validated to make sure they match. Any invalid inputs are reploretd to the user.
A user can enter their email address to get an emailed link they can follow to reset their password. The new password must be entered twice and they must match.
The hero text is in bold and the product categories are capitalised to draw attention and help explain straight away the sites purpose. A bright Call to Action button below the text aims to drive traffic to the menu, the first step in making a sale.
An eye-catching image of a burger is placed to the right. This helps grab the user's attention and make them want to order food! The burger is used as this is a new addition to Get Wurst's menu.
A small section containing introductory information is placed below the hero image. This helps to quickly confirm to the user that they are on the correct site. It offers general information on the site's purpose, so they understand its goal.
The menu shows a brief general introduction to the products.
This is followed by information about the Bratwurst on sale. Each product has an image, a name, a short description, a link to its related category and a "See More Info" button. When the image is hovered, the name link is put in a hover state. Clicking the image, name or "See More Info" button takes the user to product information about that particular item.
This is followed with the same for Chicken and then Burgers.
If a site owner is logged in, each product has a delete and edit button.
When a site owner is logged in and clicks the "DELETE" button a pop-up box asks if they are sure they want to delete the item. They can use a button to either delete the item or cancel.
When a site owner is logged in and clicks the "EDIT" button a form is displayed with the product information pre-filled. They can then change any information that needs updating and click the "Update Product" button. There is also a button to cancel editing the information.
Django widgets have been used to set the height of the textarea inputs so they don't take up unnecessary real estate on the page. The image uses the CustomClearableFileInput widget to show the currently selected image.
An image of the product is shown, as well as its name, long description, price and category link. The user can select their specified quantity by using their keyboard or the plus and minus icons either side of the input. They cannot enter an input of less than 1 or more than 9.
A primary button back to the menu and a CTA "Add to Cart" button are shown below the quantity input.
All visitors to the site can view published questions, answers and how many likes an answer has.
Anonymous users are informed that they need to login to submit questions and answers, or to like published answers.
A logged in user can submit a question and answer and like/unlike an answer. When submitting a question site users can check a checkbox if they wish to receive an email when their question has an approved answer. A question exactly the same as a published question cannot be submitted and the user is informed of this.
Questions or answers submitted by a site user has to be approved by a site owner before it is published. This is to make sure only suitable content is published and can be donw within the Django admin. Questions and answers submitted by site owners are immediatly published.
Multiple answers can be published for each question. When more than one answer is published each answer except the last has a line underneath. The last answer has a heading letting users know they can submit another answer and a form to do so.
The events page has a short introductionary paragraph. A selection of events Get Wurst has catered for are shown with an image, a name and the date in Month / Year format. The image and name are links to the event. When the image is hovered the name link is displayed as hovered.
If the current user does not have any items in their cart they are shown a message that their cart is empty and a CTA button back to the menu.
A user with items in their cart is shown an image of the item, its name and price. They can use a quantity input to adjust the quantity (no more than 9, no less than 1), or use a remove link to remove all chosen items from the cart. The cart total in shown and a primary button back to the menu and a CTA button to the checkout. The checkout button has the text "Secure Checkout" and an icon of a padlock to help build user confidence.
The checkout shows an order summary and a form to fill in payment details. The form will be pre-filled if the logged in user has previously chosen to have their information saved.
There is a primary button to adjust the cart and a CTA button to complete the order. The "Complete Order" button includes an icon of a padlock for reasons discussed previously.
When a site user is logged in they can choose to have their details saved by checking a checkbox.
Red warning text and an explanation icon informs the user how much they are about to be charged.
Django-phone-numberber-field library was used to validate the phone number.
The form can't be sent with invalid information and the user is informed where the error is.
The checkout success page shows the user the email confirmation has been sent to their email.
A primary "Back to Home Page" button is displayed beneath.
The footer at the bottom of the page includes a link to Get Wurst's Facebook business page and their Privacy Policy.
The Facebook link in the footer goes to Get Wurst's Facebook page, as seen below:
The hero image is of a burger as this is a new range Get Wurst has recently added so user attention was brought to this range.
Users can choose to sign up for a newletter so marketing information can be sent via email. Emails can also be recieved when a users question within the FAQ has an approved answer. This encourages the user to visit the site again and hopefully increase site usage.
The company is a Business to Customer business and the site was designed with this in mind. Details of products etc were described in language and using terms that a general customer would undersatand and relate to. The detail and type of information offered would have been different if the business sold to other businesses.
The brannding (colours used, style of the site etc) was kept consistant throughout the site.
Logged in users can view their profile. This shows their default information and any order history. Users can update their default information or click on a previous order number to see details of past orders.
If a user clicks the order number link they are shown the details of that order.
Add an Expand / Collapse All button to the accordion in the FAQ app.
Commit of attempt
https://github.com/mjjstockman/ecomm/commit/1cfbaedfa48c319734dae1732f133244bb2b0974
Issues:
The functionality was working as a toggle of the accordion open/closed state and "Expand all" / "Collapse all" button text. However, if one accordion was open and the others closed, their state still toggled individually.
Details on site testing can be found here.
Major bugs were added as GitHub Issues with a "bug" label.
These can be viewed in the following links:
Order email confirmation not sent
Selenium was used to automate Google Chrome web browser for testing purposes (with Pytest). An issue arose setting up the Chromedriver within GitPod. Research found that this is a fairly common issue and the best way forward would be to install and setup VS Code locally. Due to time restrictions this was not done.
An example of the Selenium produced tests can be found below:
# Generated by Selenium IDE
import pytest
import time
import json
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
class TestGetWurst():
def setup_method(self, method):
self.driver = webdriver.Chrome()
self.vars = {}
def teardown_method(self, method):
self.driver.quit()
def test_issue15Signupusingemailusernameandpassword(self):
self.driver.get("https://ecomm-gw.herokuapp.com/")
self.driver.set_window_size(1808, 1238)
self.driver.find_element(By.LINK_TEXT, "Sign Up").click()
self.driver.find_element(By.ID, "id_email").send_keys("testthis@gmail.com")
self.driver.find_element(By.ID, "id_email2").send_keys("testthis@gmail.com")
self.driver.find_element(By.ID, "id_username").send_keys("usersel")
self.driver.find_element(By.ID, "id_password1").send_keys("crimshop32")
self.driver.find_element(By.ID, "id_password2").send_keys("crimshop32")
self.driver.find_element(By.CSS_SELECTOR, ".btn:nth-child(7)").click()
A package webdriver-manager was attempted to automatically select the correct webdriver. However this could not be made to work in time.
Sign in or create an account with ElephantSQL.
If you don't have an ElephantSql account follow the below steps:
You can now create the database for the project.
In the ElephantSql dashboard select the "+ Create New Instance" button
Give the instance a name (can be anything), make sure Tiny Turtle (Free) plan is sleceted and select "Select Region" button.
If you reciece a message stating "Error: No cluster available in your-chosen-data-center yet", choose a different region.
Choose a Data center closest to you and select the "Review" button.
Back in the ElephantSQL dashboard click the databse name you just created. Copy the URL.
Sign in or create an account with Heroku
In the Heroku dashboard use the New tab to create a new app.
Name the app and choose a region.
In the Settings tab select "Reveal Congig Vars".
Add DATABASE_URL with the value of the database URL copied from ElephantSQL. This should not include qutoation marks.
Enter the following command in to your terminal to install dj_database and psycopg2. These are needed to connect to the database.
pip3 install dj_database_url==0.5.0 psycopg2
Followed by pip freeze > requirements.txt
to update requirements.txt.
Add import dj_database_url
below import os in settings.py
In the DATABASES section of the settings comment out the initial settings and place the following underneath:
DATABASES = {
'default': dj_database_url.parse('your-database-url-here')
}
Migrate the models to the database using
python3 manage.py migrate
Create a superuser and supply a username and password:
python3 manage.py migrate
Within the Django admin confirm the email address
Change the DATABSE settings to the following:
if 'DATABASE_URL' in os.environ:
DATABASES = {
'default': dj_database_url.parse(os.environ.get('DATABASE_URL'))
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
Install Gunicorn webserver:
pip3 install gunicorn
Create a file named Procfile in the root of the project and add:
web: gunicorn ecommerce.wsgi:application
Add the required dependencies to requirements.tc with:
pip3 feeze > requirements
Add the following to settings:
ALLOWED_HOSTS = ["ecomm-gw.herokuapp.com", "localhost"]
Add changes, commit and push to GitHub and Heroku
git add .
git commit -m "Deployment"
git push
git push heroku main
If you wish you can set up automatic deploys to Heroku when the project it pushed to GitHub.
Sign in or create an account with Amazon Web Server
Navigate to the AWS Console home
Search for "s3" in the search bar
Click on "Create bucket"
In the Create bucket page add the following information:
A name for the bucket. It is recomeneded to use the same name you gave your Heroku app.
A region closest to you.
Select ACLs enabled
Uncheck the "Block all public access" checkbox
Check the box beside "I acknowledge that the current settings might result in this bucket and the objects within becoming public."
Scroll to the bottom of the page and select "Create bucket"
Click on the new bucket you just created.
In the "Properties" tab scroll to the bottom of the page and in Static website hosting click "Edit".
Select "Enable" and paste the following in the the "Redirection rules – optional" at the bottom of the page:
[
{
"AllowedHeaders": [
"Authorization"
],
"AllowedMethods": [
"GET"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
In the "Permissions" tab click on the "Edit" button underneath Bucket policy
Copy the "Bucket ARN" and click on "Policy generator"
On the next page:
Select "S3 Bucket Policy"
Add * (an asterik) as the "Principal" value
Select "GetObject" in the "Actions" dropdown
Paste the "Bucket ARN" (from step 10 above) as the Amazon Resource Name (ARN)
Click "Add Statement" then "Generate Policy"
Copy the policy shown in the pop-up box
Back in the AWS "Edit bucket policy" paste the policy just copied.
At the end of the resource key but before the closing quotation mark add:
/*
and click "Save"
Enable "List" for "Everyone (public access)"
Accept the warning
Select "IAM" from the AWS menu and then "User groups" from the menu on the left hand side.
Click the "Create group" button
Give the group a name and click "Next step", then on the next page "Next step" agin.
Click the "Create group" button
Select "Polices" from the menu, then "Create policy" button.
In the JSON tab select "Import managed policy" link
Add "s3" into the search bar, select "AmazonS3FullAccess" and click "Import"
Paste in the Bucket ARN and the Bucket ARN followed by /* as the Resource values
Click "Review policy" and add a name and description and click "Create policy"
Select "Groups" from the menu and select the group you made in Step 3.
Click "Attach policy", search and select the policy just created.
Click "Attach policy"
Select "Users" in the menu, then click "Add user"
Add a user name and check the "Programatic access" checkbox for Access type
Select "Next: Permissions"
On the following page check the name of your added user group
Click through the next pages until you can click the "Create user" button
Donload the .csv file
pip3 install boto3
pip3 install django-storages
pip3 freeze > requirenets.txt
Add "storages", to INSTALLED_APPS in seetings.py
Paste the following in settings.py:
if 'USE_AWS' in os.environ:
# Cache control
AWS_S3_OBJECT_PARAMETERS = {
'Expires': 'Thu, 31 Dec 2099 20:00:00 GMT',
'CacheControl': 'max-age=94608000',
}
# Bucket Config
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = 'eu-west-2'
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
# Static and media files
STATICFILES_STORAGE = 'custom_storages.StaticStorage'
STATICFILES_LOCATION = 'static'
DEFAULT_FILE_STORAGE = 'custom_storages.MediaStorage'
MEDIAFILES_LOCATION = 'media'
# Override static and media URLs in production
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{STATICFILES_LOCATION}/'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIAFILES_LOCATION}/'
Select "Reveal Config Vars" in the "Settings" tab
Add the variables "AWS_ACCESS_KEY_ID" and "AWS_SECRET_ACCESS_KEY" with the values from the credentails.csv file from Step 6 in Add a user to the group above
Add an additioanl "USE_AWS" variable with the key of "True"
Create a file named "custom_storages.py"
Paste in the following:
from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage
class StaticStorage(S3Boto3Storage):
location = settings.STATICFILES_LOCATION
class MediaStorage(S3Boto3Storage):
location = settings.MEDIAFILES_LOCATION
Add a folder named "media" in the S3 bucket page
Click "Upload" in the media folder
Select the images used on the site and click "Next"
Select "Grant public read access to this object(s)" under Manage public permissions, click "Next" then "Next" again
Click "Upload"
Sign in or create an account with Stripe
Click "Developers" from the menu on the left hand side and then "API keys"
Copy the "Publishable key" and "Secret key"
Add these to Heroku Config Vars as STRIPE_PUBLIC_KEY and STRIPE_SECRET_KEY respectivaely.
Click "Webhooks" from the Developers tab and then "+ Add endpoint"
Enter the following as the "Endpoint URL":
https://ecomm-gw.herokuapp.com/checkout/wh/
Select "Recieve all events" and then "Add endpoint"
Copy the "Signing secret"
Add the "Signing secret" to Heroku Config Vars as STRIPE_WH_SECRET
Log into GitHub or create an account.
Select the GitHub Repository.
Click "Fork" at the top right of the page.
The repository will be copied into your GitHub account.
Log into GitHub or create an account.
Select the GitHub Repository.
Click on the Code button.
Copy the provided SSH link.
Open Terminal.
Navigate into the directory you want to clone the repositroy to.
Type git clone and paste the copied URL.
$ git clone https://github.com/mjjstockman/ecomm
Press Enter.
In order to allow users to register and login using Google. See here for detailed instructions.
Many thanks to the following which were used throughout the creation of this site:
The logo was created by Alex Mench who kindly gave permission for its use.
All other images were taken from Unsplash.
Images were resized and comnverted to webp files using:
https://www.simpleimageresizer.com/ https://tinypng.com/
[lighthouse report]((static/images/readme/lighthouse/first/lighthouse report viewer.dpf))
The following article helped to improve elimination of render blocking resources: https://blog.logrocket.com/9-tricks-eliminate-render-blocking-resources/
Main render-blocking resources are Stripe (with 447,926 bytes and 78% being unused) and Bootstrap (with 194,696 and 87 % being unused). It was decided not to purge these of unused CSS incase the project needs expanding on or revision at a future date.
Correct ratio of logo Lighthouse (static/images/readme/lighthouse/first/logo-ratio.png)
Gave logo explicit width and height.
Saw space between exception value, checked code
class CustomClearableFileInput(ClearableFileInput):
clear_checkbox_label = _("Remove")
initial_text = _("Current Image")
input_text = _("")
template_name = "products/custom_widget_templates/\
custom_clearable_file_input.html"
Incorrect line break.
template_name = "products/custom_widget_templates/" \
"custom_clearable_file_input.html"
(static/images/readme/bugs/faq-label-error.png) COMMIT 501
Wet menu/all template
<h2>Bratwursts</h2>
<div class="row">
<div class="col-12">
<div class="row">
{% for product in products %}
{% if product.category|slugify == 'bratwurst' %}
<div class="mb-3 col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<div class="image-container">
<!-- ADD ARIA LABEL -->
<a href="https://github.com/mjjstockman/doze/blob/main/{% url 'product_detail' product.id %}">
<img class="card-img-top card-img" src="https://github.com/mjjstockman/doze/raw/main/{{ product.image.url }}"
alt="{{ product.name }}" class="h-50">
</a>
</div>
<div>
<h1 class="card-title">{{ product.name }}</h1>
</a>
<p class="card-text">{{ product.short_description }}</p>
</div>
<div class="mb-2 d-grid">
<a href="https://github.com/mjjstockman/doze/blob/main/{% url 'products' %}?category={{ product.category }}"
class="mb-2 card-subtitle text-muted text-decoration-none">
{{ product.category }}
</a>
<a href="https://github.com/mjjstockman/doze/blob/main/{% url 'product_detail' product.id %}" class="card-link btn btn-cta"
role="button">
See More Info
</a>
<!-- Button trigger modal -->
{% if request.user.is_superuser %}
<div class="card-body border-top border-1 border-dark">
<div class="gap-2 mb-2 d-grid">
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#deleteProductModal">
DELETE
</button>
<a class="btn btn-secondary" href="https://github.com/mjjstockman/doze/blob/main/{% url 'edit_product' product.id %}">
Edit
</a>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="deleteProductModal" tabindex="-1"
aria-labelledby="deleteProductModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteProductModalLabel">Modal title
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure you want to delete {{ product.name }}?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Cancel</button>
<a class="btn btn-danger"
href="https://github.com/mjjstockman/doze/blob/main/{% url 'delete_product' product.id %}">Delete</a>
</div>
</div>
</div>
</div> <!-- closes modal -->
{% endif %} <!-- close superuser-->
</div>
</div>
</div>
</div>
{% endif %}
{% empty %}
<h2>NO PRODUCTS YET</h2>
{% endfor %}
</div> <!-- closes row -->
<h2>Currywurst</h2>
<div class="row">
{% for product in products %}
{% if product.category|slugify == 'currywurst' %}
<div class="mb-3 col-md-6 col-lg-4">
<div class="card">
Used partial template to DRY
''' class QuestionForm(forms.ModelForm): def init(self, args, **kwargs): super(QuestionForm, self).init(args, **kwargs)
self.fields['body'].label = "Question"
Saw refering to a list var
def all(request):
products = get_list_or_404(Product)
cart = request.session.get("cart", {})
query = None
categories = None
Products var is a list. Change back to Product.objects.all
def all(request): products = Product.objects.all() cart = request.session.get("cart", {}) query = None categories = None
Django field labels not working once added css
class QuestionForm(forms.ModelForm):
email_on_answer = forms.CharField(widget=forms.CheckboxInput(attrs={"class": "rounded-0"}))
body = forms.CharField(widget=forms.TextInput(attrs={"class": "rounded-0"}))
class Meta:
model = Question
fields = ['body', 'email_on_answer']
labels = {
'body': 'Question',
}
Add as below instead....
''' class QuestionForm(forms.ModelForm): def init(self, args, **kwargs): super(QuestionForm, self).init(args, **kwargs)
self.fields['body'].label = "Question"
'''