Welcome to Hanqout. This project was birthed from my interest in how to care for developer's mental health. In my previous experience as a UI/UX designer I had always collaborated with developers and I had a glimpse of how time consuming coding was, however on switching my role to being a full stack developer I understood the demands and how much we developers love to code so I thought of building an application through which developers can hangout, relax and be refreshed to code again. This project was my fourth project for the Software Engineering Immersive course at General Assembly London.
This was a solo project and I had 9 days to build a fullstack application using my own RESTful API with Python(Django) as the backend language and JavaScript(React.js) for the frontend.
Solo or Group? You are free to work alone or in a group. Both ways have their pros and cons. Remember if you are working in a team that you are all on the same page and working towards the same goal.- Build a full-stack application by making your own backend and your own front-end.
- Use a Python Django API using Django REST Framework to serve your data from a Postgres database.
- Consume your API with a separate front-end built with React.
- Be a complete product which most likely means multiple relationships and CRUD functionality for at least a couple of models.
- Implement thoughtful user stories/wireframes that are significant enough to help you know which features are core MVP and which you can cut.
- Visually impressive design.
- React Hooks is optional for this project.
- Time-frame: 9 days
- Python
- Django
- PostgreSQL
- React
- React Hooks
- Insomnia
- Axios
- Sass
- Bulma
- Git
- GitHub
- Figma
- Trello
Repository link: https://github.com/victoriaolanipekun/HANQOUT
To download the source code click the clone button. Run the following commands in the terminal:
-
To install all the packages in the frontend directory:
yarn
-
To run the app in your localhost: In front :
yarn start
Backend: To install all the packages in the root directory:
- Install Django and a shell in the root directory:
pip install pipenv
pipenv install django
pipenv shell
- Create the postgreSQL database:
pipenv install psycopg2-binary
createdb hanqout
- Migrate everything from the backend:
python manage.py migrate
- To seed your database run the following command, in the following order:
python manage.py loaddata jwt_users/seeds.json
python manage.py loaddata locations/seeds.json
python manage.py loaddata comments/seeds.json
python manage.py loaddata categories/seeds.json
python manage.py loaddata hanqout/seeds.json
I decided to manage the project sprints and development using Trello by breaking it up into several sprints with deadlines in days. The features were also listed in order of priority to help me focus while I build.
As soon as I was happy with my planning, I began working on the backend as my first step was to create a Django project containing my two apps. The project contains its own individual set of URLs which we would use to distinguish the API endpoints to retrieve any data in the frontend. The project has its own settings where I configured the apps, middleware, databases, our custom made user model, and authentications for the apps to use.
Django comes with an in-built CMS whereby an administrative user can directly add, update and delete database records. I used Django REST framework to build the API, as it has powerful functionality but allows for customisation.
In order to access Django’s CMS, a superuser is needed so I created one, also I created apps. For each app created, a URL, Model, and Views Python files are automatically created. These newly created apps needed to be included into the main project’s installed apps section to track changes.
Lastly in my setup I configured my database to be PostgreSQL. By default, Django uses SQLite. To change this, I modified the database section in the main project settings to use PostgreSQL.
Once the project had been set up I went ahead in building my two apps. For each app I created a model, and registered them in their respective admin.py
file.
- Hanqout Model:
class Hanqout(models.Model):
title = models.CharField(max_length=50, default=None)
image = models.CharField(max_length=500, default=None)
description = models.CharField(max_length=500, default=None)
venue = models.CharField(max_length=200, default=None)
date = models.CharField(max_length=200, default=None)
time = models.CharField(max_length=200, default=None)
price = models.CharField(max_length=200, default=None)
keywords = models.CharField(max_length=200)
worth_a_go = models.BooleanField(default=True, null=True)
categories = models.ManyToManyField("categories.Category", related_name="hanqouts")
locations = models.ManyToManyField("locations.Location", related_name="hanqouts")
owner = models.ForeignKey(
"jwt_auth.User",
related_name="hanqouts",
on_delete = models.CASCADE
)
def __str__(self):
return f"{self.title} - {self.description}"
-
The Hanqout Class model consists of a title, image, description, venue, date, time, price, keywords, worth_a_go, categories and locations.
-
A comment model was created and to be implemented as a bonus feature for users to comment on hanqouts (activities) but due to timeframe limitations wasn't implemented on the frontend.
-
The model, had the following relationships:
- A one-to-many relationship with comment model, signifying many comments to one hanqout;
- A many-to-many relationship with location model, signifying there can be many locations to many hanqouts.
- A many-to-many relationship with category model, signifying there can be many categories to many hanqouts.
- Comment Model:
class Comment(models.Model):
text = models.TextField(max_length=300)
created_at = models.DateTimeField(auto_now_add=True)
hanqouts = models.ForeignKey(
"hanqout.Hanqout", # which model are we creating the realtionship with
related_name="comments", # what name the field will have on the other model, comments on hanqouts,
on_delete= models.CASCADE # if a hanqout is deleted, also delete the comments
)
owner = models.ForeignKey(
"jwt_auth.User",
related_name="comments",
on_delete = models.CASCADE
)
def __str__(self):
return f"Comments: {self.text}"
-
The Comment Class model consists of a text, created_at, hanqouts and owner.
-
The Comment Class model has a one to many relationship with hanqouts model and custom user model, from the JWT Auth App. This shows that each hanqout or user can have many comment classes. These comment classes can then be displayed on the indexpage on the front-end.
- Category Model:
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return f"Category: {self.name}"
- The Category Class model consists of a name. This model has a many to many relationship with the hanqout model which indicates that there are many hanqouts in many categories.
- Location Model:
class Location(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return f"Location: {self.name}"
- The Location Class model consists of a name too. This model has a many to many relationship with the hanqout model which indicates that there are many hanqouts in many categories.
2. JWT_AUTH Django by default already has email, password & password confirmation & usernames, however it doesn't make email required so I changed that by defining these fields and made them required by specifying unique as true:
class User(AbstractUser):
email = models.CharField(max_length=50, unique=True)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
profile_image = models.CharField(max_length=300)
- Specifying the email field as true disables users from registering with the same email. The profile_image field allows my users to upload their own image to the database.
Next, I created serializers to enable Django to communicate with the PostgreSQL database. The serializer translates the data structure from JSON into a format that can be stored or transmitted and reconstructed later. I have included a code snippet of the JWT_AUTH SERIALIZER.
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
password_confirmation = serializers.CharField(write_only=True)
def validate(self, data):
password = data.pop('password')
password_confirmation = data.pop('password_confirmation')
if password != password_confirmation:
raise ValidationError({'password_confirmation': 'Passwords do not match'})
try:
password_validation.validate_password(password=password)
except ValidationError as err:
raise ValidationError({'password': err.messages})
data['password'] = make_password(password)
return data
class Meta:
model = User
fields = ('id', 'username', 'password_confirmation', 'password', 'email')
I then, implemented Django REST framework to create the views to render my backend data and enable CRUD functionality. I have included a code snippet of the hanqout views.
class HanqoutListView(APIView):
permission_classes = (IsAuthenticatedOrReadOnly, )
def get(self, _request):
hanqouts = Hanqout.objects.all() # get everything from the hanqouts table in the db
serialized_hanqouts = PopulatedHanqoutSerializer(hanqouts, many=True) # transform data into python by running through serializer
return Response(serialized_hanqouts.data, status=status.HTTP_200_OK) # return data and status code
def post(self, request):
request.data['owner'] = request.user.id
hanqout_to_add = HanqoutSerializer(data=request.data)
if hanqout_to_add.is_valid():
hanqout_to_add.save()
return Response(hanqout_to_add.data, status=status.HTTP_201_CREATED)
return Response(hanqout_to_add.errors, status=status.HTTP_422_UNPROCESSABLE_ENTITY)
In my project I wrote urls in order to access each app:
urlpatterns = [
path('', HanqoutListView.as_view()),
path('locations/<int:pk>/', HanqoutLocation.as_view()),
path('categories/<int:pk>/', HanqoutCategory.as_view()),
path('<int:pk>/', HanqoutDetailView.as_view())
]
I used insomnia to test out all of my routes and requests.
Now that I was done with the backend I moved on to building the frontend.
The Frontend was built using React Hooks.The landing page prompts users to find or create hanqouts, and also to either sign up or to login using the action buttons to redirect them to the appropriate page. As a user you can actually view the hanqouts i.e events that had been created by others however to create a new hanqout sign up or registration is required. I styled the homepage using the Bulma CSS Framework.
Depending on what the user has selected from the landing page, a redirection is made to either the register or login components. A code snippet for registeration is shown below using the React hook's formdata
const Register = () => {
const history = useHistory()
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
password_confirmation: '',
first_name: '',
last_name: '',
})
const [errors, setErrors] = useState({
username: '',
email: '',
password: '',
password_confirmation: '',
first_name: '',
last_name: '',
})
const handleChange = (event) => {
const newFormData = { ...formData, [event.target.name]: event.target.value }
const newErrors = { ...errors, [event.target.name]: '' }
setFormData(newFormData)
setErrors(newErrors)
}
const handleSubmit = async event => {
event.preventDefault()
try {
await axios.post('/api/auth/register/', formData)
history.push('/login')
} catch (err) {
setErrors(err.response.data.errors)
}
}
The error handling function was also included in the build.
The hanqout page is were the various created activities can be viewed and joined. Also a new hanqout can be created from this page. The index pages can be accesed from the home page as mentioned above or via the navbar. Here the user can see the list of activities. useEffect(() => {
const getData = async () => {
try {
const { data } = await axios.get('/api/hanqout/')
setHanqouts(data)
console.log('data', data)
} catch (err) {
setHasError(true)
}
}
getData()
}, [])
Once the page renders, a GET request is made to the ALL hanqout endpoint and this is stored into the hanqout state.
The user is also able to filter by location should they want to see hanqouts around them or in a particular vicinity within the UK. That was defined in the getHanqoutByLocation
function as seen below:
const getHanquotByLocation = async (locate) => {
try {
let result = null
if (locate !== '') {
result = await axios.get(`/api/hanqout/locations/${locate}`)
} else {
result = await axios.get('/api/hanqout/')
}
let data = null
if (result.data.id !== undefined) {
data = []
data.push(result.data)
} else {
data = result.data
}
setHanqouts(data)
} catch (err) {
setHasError(true)
}
}
In the render method, I rendered a drop down onto the page with values that are the class types, and a search form is visible which also has a handleChange event handler, where the getHanquotByLocation
function is called and this gets the new hanquots based on the location:
const handleChange = (event) => {
console.log('Hello=>', event.target.value)
getHanquotByLocation(event.target.value)
}
const HanqoutNew = () => {
const history = useHistory()
const [formData, setFormData] = useState({
title: '',
image: '',
description: '',
venue: '',
date: '',
time: '',
price: '',
keywords: '',
})
const [errors, setErrors] = useState({
title: '',
image: '',
description: '',
venue: '',
date: '',
time: '',
price: '',
keywords: '',
})
const handleChange = (event) => {
const newFormData = { ...formData, [event.target.name]: event.target.value }
const newErrors = { ...errors, [event.target.name]: '' }
setFormData(newFormData)
setErrors(newErrors)
}
const handleSubmit = async (event) => {
event.preventDefault()
const cat = parseInt(formData.categories)
const categories = Array.from(String(cat), Number)
const locate = parseInt(formData.locations)
const locations = Array.from(String(locate), Number)
formData.categories = categories
formData.locations = locations
try {
const result = await axios.post(
'/api/hanqout/',
formData,
{
headers: { Authorization: `Bearer ${getTokenFromLocalStorage()}` },
}
)
console.log('Result=>', result)
history.push('/hanqout')
} catch (err) {
setErrors(err.response.data.errors)
}
}
const handleLocationOption = option => {
setFormData({ ...formData, location: option })
}
-
Implement comments to each hanqout class.
-
Implement an experience page where users can share their hanqout experiences.