Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[flake8]
ignore = E501,E241,E111,E117,E402,F401,E303,E305,E722

exclude =
.git,
__pycache__,
venv\,
.github\
.pytest_cache\
*.pyc,
.env\,
env\,
venv\,
.coverage,
templates\,
static\,

82 changes: 82 additions & 0 deletions .github/workflows/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: GitHub Actions Demo
run-name: ${{ github.actor }} CI for oc-lettings-site 🚀
on:
push:
pull_request:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
DEBUG: "False"
ALLOWED_HOSTS: "127.0.0.1,localhost,0.0.0.0,oc-lettings-site-latest-05t2.onrender.com"
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
run: |
pytest --cov --cov-fail-under=80

flake8:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Flake8
run: |
flake8 --count

build:
needs: [test, flake8]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' && github.event_name == 'push'

steps:
- uses: actions/checkout@v4

- name: Log in to DockerHub
run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin

- name: Build the Docker image
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: |
docker build . --file Dockerfile \
--build-arg SECRET_KEY="${SECRET_KEY}" \
--tag ${{ secrets.DOCKERHUB_USERNAME }}/oc-lettings-site:${{ github.sha }} \
--tag ${{ secrets.DOCKERHUB_USERNAME }}/oc-lettings-site:latest

- name: Push image to DockerHub
run: |
docker push ${{ secrets.DOCKERHUB_USERNAME }}/oc-lettings-site:${{ github.sha }}
docker push ${{ secrets.DOCKERHUB_USERNAME }}/oc-lettings-site:latest

deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' && github.event_name == 'push'

steps:
- name: Deploy to Render
env:
deploy_url: ${{ secrets.RENDER_DEPLOY_HOOK_URL }}
run: |
curl "$deploy_url"
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
**/__pycache__
*.pyc
venv
.coverage
venv/
.env
staticfiles/
reports/
.pytest_cache/
Dockerfile1
Dockerfile1
docker-compose.yml.old
compose.yml
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.10-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
ARG SECRET_KEY="temporary-insecure-key-for-local-build-only-do-not-use-in-production"
ENV SECRET_KEY=${SECRET_KEY}

COPY . .

RUN python manage.py collectstatic --noinput

EXPOSE 8000

CMD ["gunicorn", "oc_lettings_site.wsgi:application", "--bind", "0.0.0.0:8000"]
25 changes: 25 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: "3.13"

services:
quality-check:
build: .
volumes:
- .:/app
working_dir: /app
entrypoint: ["/bin/sh", "-c"]
command: >
flake8 . --count --max-complexity=10 --max-line-length=88 --exclude=venv,migrations,__pycache__ &&
pytest --cov=. --cov-report=term-missing --cov-fail-under=80

web:
build: .
command: gunicorn oc_lettings_site.wsgi:application --bind 0.0.0.0:8000 --reload
ports:
- "8000:8000"
env_file:
- .env
volumes:
- .:/app
depends_on:
quality-check:
condition: service_completed_successfully
25 changes: 25 additions & 0 deletions docker-compose.yml.old
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: "3.13"

services:
quality-check:
build: .
volumes:
- .:/app
working_dir: /app
entrypoint: ["/bin/sh", "-c"]
command: >
flake8 . --count --max-complexity=10 --max-line-length=88 --exclude=venv,migrations,__pycache__ &&
pytest --cov=. --cov-report=term-missing --cov-fail-under=80

web:
build: .
command: gunicorn oc_lettings_site.wsgi:application --bind 0.0.0.0:8000 --reload
ports:
- "8000:8000"
env_file:
- .env
volumes:
- .:/app
depends_on:
quality-check:
condition: service_completed_successfully
Empty file added lettings/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions lettings/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin
from .models import Address, Letting

admin.site.register(Address)
admin.site.register(Letting)
11 changes: 11 additions & 0 deletions lettings/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig


class LettingsConfig(AppConfig):
"""
AppConfig for the 'lettings' application.

This class is used by Django to configure and register
the 'lettings' app within the project.
"""
name = 'lettings'
36 changes: 36 additions & 0 deletions lettings/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 3.0 on 2026-01-04 09:01

import django.core.validators
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Address',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(9999)])),
('street', models.CharField(max_length=64)),
('city', models.CharField(max_length=64)),
('state', models.CharField(max_length=2, validators=[django.core.validators.MinLengthValidator(2)])),
('zip_code', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(99999)])),
('country_iso_code', models.CharField(max_length=3, validators=[django.core.validators.MinLengthValidator(3)])),
],
),
migrations.CreateModel(
name='Letting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=256)),
('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='lettings.Address')),
],
),
]
Empty file added lettings/migrations/__init__.py
Empty file.
55 changes: 55 additions & 0 deletions lettings/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django.db import models
from django.core.validators import MaxValueValidator, MinLengthValidator


class Address(models.Model):
"""
Represents a physical address linked to a letting.

Stores structured data for the street, city, and postal information.
Each address is unique and associated with a single letting.

Attributes:
number (int): Street number, max 4 digits.
street (str): Street name, up to 64 characters.
city (str): City name, up to 64 characters.
state (str): Two-letter state abbreviation (e.g., 'CA').
zip_code (int): Postal code, max 5 digits.
country_iso_code (str): Three-letter country code (ISO 3166-1 alpha-3).
"""
class Meta:
verbose_name_plural = "addresses"

number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)])
street = models.CharField(max_length=64)
city = models.CharField(max_length=64)
state = models.CharField(max_length=2, validators=[MinLengthValidator(2)])
zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)])
country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)])

def __str__(self) -> str:
"""
Returns a readable string representation of the address.
"""
return f'{self.number} {self.street}'


class Letting(models.Model):
"""
Represents a property available for rent.

Each letting is linked to a unique address and has a descriptive title
to identify the property in the interface.

Attributes:
title (str): Name or label of the property.
address (Address): One-to-one relationship to the property's address.
"""
title = models.CharField(max_length=256)
address = models.OneToOneField(Address, on_delete=models.CASCADE)

def __str__(self) -> str:
"""
Returns a readable string representation of the letting.
"""
return self.title
46 changes: 46 additions & 0 deletions lettings/templates/lettings/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{% extends "base.html" %} {% block title %}Lettings{% endblock title %} {% block content %}

<div class="container px-5 py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="page-header-ui-title mb-3 display-6">Lettings</h1>
</div>
</div>
</div>

<div class="container px-5">
<div class="row gx-5 justify-content-center">
<div class="col-lg-10">
<hr class="mb-0" />
{% if lettings_list %}
<ul class="list-group list-group-flush list-group-careers">
{% for letting in lettings_list %}
<li class="list-group-item">
<a href="{% url 'lettings:letting' letting_id=letting.id %}"
>{{ letting.title }}</a
>
</li>
{% endfor %}
</ul>
{% else %}
<p>No lettings are available.</p>
{% endif %}
</div>
</div>
</div>

<div class="container px-5 py-5 text-center">
<div class="justify-content-center">
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'index' %}">
Home
</a>
<a
class="btn fw-500 ms-lg-4 btn-primary px-10"
href="{% url 'profiles:index' %}"
>
Profiles
</a>
</div>
</div>

{% endblock %}
45 changes: 45 additions & 0 deletions lettings/templates/lettings/letting.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{% extends "base.html" %} {% load static %} {% block title %}{{ title }}{%endblock title %} {% block content %}

<div class="container px-5 py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="page-header-ui-title mb-3 display-6">{{ title }}</h1>
</div>
</div>
</div>

<div class="container px-5 py-5 text-center">
<div class="card">
<div class="card-body">
<div class="icon-stack icon-stack-lg bg-primary text-white mb-3">
<i data-feather="home"></i>
</div>
<p>{{ address.number }} {{ address.street }}</p>
<p>{{ address.city }}, {{ address.state }} {{ address.zip_code }}</p>
<p>{{ address.country_iso_code }}</p>
</div>
</div>
</div>

<div class="container px-5 py-5 text-center">
<div class="justify-content-center">
<a
class="btn fw-500 ms-lg-4 btn-primary px-10"
href="{% url 'lettings:index' %}"
>
<i class="ms-2" data-feather="arrow-right"></i>
Back
</a>
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'index' %}">
Home
</a>
<a
class="btn fw-500 ms-lg-4 btn-primary px-10"
href="{% url 'profiles:index' %}"
>
Profiles
</a>
</div>
</div>

{% endblock %}
Empty file added lettings/tests/__init__.py
Empty file.
Loading