Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
VladIakimenko committed May 30, 2023
0 parents commit 63e121b
Show file tree
Hide file tree
Showing 39 changed files with 820 additions and 0 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Backend for Web-Based Advertisement Platform

### Tech task

The project task is to develop a backend for a classified ads web application. This platform necessitates the capacity for authorized users to create ads, while ad viewing functionality should not be restricted to authorization.
Additionally, safeguards must be put in place to ensure that ads cannot be falsely created on behalf of others. A regulation should be implemented that restricts users to no more than ten open ads concurrently.
Ads on the platform will feature two statuses: OPEN and CLOSED. The backend should facilitate updates and deletions of ads by their original creators only. Alongside this, an administration function should be introduced, enabling administrators to edit or delete any ad as required.
A favorites feature should be incorporated as well, allowing users to mark specific ads as favorites for easy access.
The backend should also provide filtration capabilities for ads based on their status and date of creation, to be utilized by the frontend.
A separate draft status should be implemented. This status renders the ad visible only to its creator, effectively hiding it from other users until the status is changed.
In terms of system security, rate limits are to be enforced to protect against potential bot activity and malicious usage. Specifically, non-authorized users should be limited to 10 requests per minute, with authorized users permitted up to 20 requests per minute.

### Realization features
**Advertisements:** The Advertisement model is defined in the models.py file, which includes fields for title, description, status, creator, created_at, updated_at, and a draft boolean field. Advertisement statuses are defined in the AdvertisementStatusChoices class with options for OPEN and CLOSED. The draft field is used to mark an advertisement as a draft, making it visible only to the creator.
**Advertisement Creation:** In the serializers.py, the AdvertisementSerializer redefines the create method where the creator is automatically set to the current authenticated user. Meanwhile, a validation method checks the number of open ads created by the user and throws a validation error if the user has reached the limit of 10 open advertisements.
**Favorites:** A Favorite model is also defined in the models.py, which represents a many-to-many connection between a User and an Advertisement. The FavoriteSerializer in the serializers.py file provides the methods for serializing the data of a Favorite instance. The representation of favorites differ, depending on whether a stuff user requests the data or an ordinary authenticated user, allowing admins to see all of the user-adverisement pairs, while a user can only operate his own favorites list. When a Favorite is created, the serializer checks if the advertisement is already in the user's favorites and raises a validation error if it is.
**Permissions:** In the views.py file, the AdvertisementViewSet and FavoriteViewSet classes define the permissions required for different actions. The AdvertisementViewSet includes a get_permissions method that checks if the authenticated user is the ad's creator before allowing update or deletion actions. This method also allows admins to gain unlimitted access to all entries.
The use of Django's Q objects in the AdvertisementViewSet's get_queryset method allows complex database queries, enabling the function to return different querysets based on whether the user is staff, anonymous, or a regular authenticated user. This is an example of how the project provides a different level of access based on user type.
**Database:** The project uses PostgreSQL as its database, as specified in the settings.py file. The credentials for the database are fetched from environment variables for improved security.
Additionally, the implementation uses the TokenAuthentication class from Django Rest Framework for authentication, which provides secure, token-based authentication for the application's users.
**Rate Limits:** Request rate limiting is set in the settings.py file using Django Rest Framework's throttle classes. Non-authorized users are limited to 10 requests per minute, and authorized users are limited to 20 requests per minute.

### Setup and Run

To set up and run follow standard instruction for a Django app:
- Clone the Repository: The project is stored in a git repository, you will need to clone it to your local system.
- Virtual Environment: Create a virtual environment using Python's venv module or a tool like virtualenv. This isolates the dependencies for the project.
- Install Dependencies: With your virtual environment activated, install the project's dependencies. They are listed in requirements.txt file. You can install them with 'pip install -r requirements.txt'.
- Set Up .env: Rename .env (example) to .env and fill in your environment variables. These will include things like your SECRET_KEY, database information, etc.
- Database Setup: Create your database. Ensure that the database settings in your .env file match your actual database setup.
- Run Migrations: Django uses migrations to manage database schema. You can apply these with 'python manage.py migrate'.
- Run the Server: Finally, you can run your server with 'python manage.py runserver'. By default, this will start the server on localhost:8000.

This setup assumes a development environment. For a production deployment, you will need to follow the standard procedures for your specific production environment. This usually involves setting up a production-ready web server like Gunicorn or uWSGI, setting up a reverse proxy like Nginx, and securing your application. For more information, refer to the Django deployment checklist.
Set Up for Testing

The application offers a unique feature for setting up the environment for a quick testing, to check the application features:
- Create Test Users: Run 'python manage.py create_test_users'. This command will automatically create several test users and an admin user, saving their authentication tokens to the http-client.env.json file for use with HTTP clients.
- Connect Virtual Environment for your http-client: In your HTTP client, connect to the 'dev' virtual environment from the http-client.env.json file. The method to do this may vary depending on your client. Generally, this can be done through a setting or command such as @env http-client.env.json.
- Maintain Request Order: When running the requests in request-examples.http, it's important to maintain the order of execution. Some requests depend on the state of the application after previous requests.
- Throttling and Constraints Checks: The request-examples.http file includes requests designed to check rate limiting (throttling) and constraints on the number of 'OPEN' advertisements. Please follow the comments in the file to execute these tests correctly.

Note: With these steps, there's no need to manually create users through the Django admin interface, making the setup process smoother and easier to automate. This unique setup feature allows for robust, thorough testing of the application's functionality, ensuring the quality and reliability of your application.
Binary file added __pycache__/manage.cpython-310.pyc
Binary file not shown.
Empty file added advertisements/__init__.py
Empty file.
Binary file added advertisements/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file added advertisements/__pycache__/admin.cpython-310.pyc
Binary file not shown.
Binary file added advertisements/__pycache__/apps.cpython-310.pyc
Binary file not shown.
Binary file added advertisements/__pycache__/filters.cpython-310.pyc
Binary file not shown.
Binary file added advertisements/__pycache__/models.cpython-310.pyc
Binary file not shown.
Binary file not shown.
Binary file added advertisements/__pycache__/views.cpython-310.pyc
Binary file not shown.
3 changes: 3 additions & 0 deletions advertisements/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions advertisements/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class AdvertisementsConfig(AppConfig):
name = 'advertisements'
13 changes: 13 additions & 0 deletions advertisements/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters

from advertisements.models import Advertisement


class AdvertisementFilter(filters.FilterSet):
"""Фильтры для объявлений."""
created_at_before = filters.DateFilter(field_name="created_at", lookup_expr='lte')
created_at_after = filters.DateFilter(field_name="created_at", lookup_expr='gte')

class Meta:
model = Advertisement
fields = ['created_at_before', 'created_at_after']
Binary file not shown.
45 changes: 45 additions & 0 deletions advertisements/management/commands/create_test_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
import json
import os


class Command(BaseCommand):
help = 'Creates test users and saves their tokens to http-client environment file ("http-client.env.json")'

def handle(self, *args, **options):
usernames = ['user1', 'user2', 'user3']
password = 'test_password'
admin_username = 'admin1'
admin_password = 'admin_password' # Change this to a secure password

env_file = 'http-client.env.json'
if os.path.exists(env_file):
with open(env_file, 'r') as file:
data = json.load(file)
else:
data = {}

for i, username in enumerate(usernames, start=1):
user, created = User.objects.get_or_create(username=username)
if created:
user.set_password(password)
user.save()

token, _ = Token.objects.get_or_create(user=user)
data['dev'][f'token{i}'] = str(token.key)

# Add the admin user
admin_user, created = User.objects.get_or_create(username=admin_username, is_staff=True)
if created:
admin_user.set_password(admin_password)
admin_user.save()

admin_token, _ = Token.objects.get_or_create(user=admin_user)
data['dev']['adminToken'] = str(admin_token.key)

with open(env_file, 'w') as file:
json.dump(data, file, indent=4)

self.stdout.write(self.style.SUCCESS('Successfully created users and saved tokens to "http-client.env.json"'))
29 changes: 29 additions & 0 deletions advertisements/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 3.1.2 on 2020-10-12 02:26

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Advertisement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.TextField()),
('description', models.TextField(default='')),
('status', models.TextField(choices=[('OPEN', 'Открыто'), ('CLOSED', 'Закрыто')], default='OPEN')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
29 changes: 29 additions & 0 deletions advertisements/migrations/0002_alter_advertisement_id_favorite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.1 on 2023-05-29 08:17

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('advertisements', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='advertisement',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.CreateModel(
name='Favorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('advertisement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='advertisements.advertisement')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to=settings.AUTH_USER_MODEL)),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.1 on 2023-05-29 15:32

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('advertisements', '0002_alter_advertisement_id_favorite'),
]

operations = [
migrations.AddField(
model_name='advertisement',
name='draft',
field=models.BooleanField(default=False),
),
migrations.AlterUniqueTogether(
name='favorite',
unique_together={('user', 'advertisement')},
),
]
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35 changes: 35 additions & 0 deletions advertisements/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django.conf import settings
from django.db import models


class AdvertisementStatusChoices(models.TextChoices):
OPEN = "OPEN", "Открыто"
CLOSED = "CLOSED", "Закрыто"


class Advertisement(models.Model):
title = models.TextField()
description = models.TextField(default='')
status = models.TextField(
choices=AdvertisementStatusChoices.choices,
default=AdvertisementStatusChoices.OPEN,
)
creator = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
created_at = models.DateTimeField(
auto_now_add=True,
)
updated_at = models.DateTimeField(
auto_now=True,
)
draft = models.BooleanField(default=False)


class Favorite(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='favorites')
advertisement = models.ForeignKey(Advertisement, on_delete=models.CASCADE, related_name='favorites')

class Meta:
unique_together = ('user', 'advertisement')
98 changes: 98 additions & 0 deletions advertisements/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from django.shortcuts import get_object_or_404
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from advertisements.models import Advertisement, Favorite


class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
is_staff = serializers.BooleanField(write_only=True)
is_superuser = serializers.BooleanField(write_only=True)

class Meta:
model = User
fields = (
'id',
'username',
'first_name',
'last_name',
'password',
'is_staff',
'is_superuser',
)

def validate_password(self, value):
validate_password(value)
return value

def create(self, validated_data):
password = validated_data.pop('password')
user = User(**validated_data)
user.set_password(password)
user.save()
return user



class AdvertisementSerializer(serializers.ModelSerializer):
creator = UserSerializer(
read_only=True,
)

class Meta:
model = Advertisement
fields = (
'id',
'title',
'description',
'creator',
'status',
'created_at',
'draft'
)

def create(self, validated_data):
validated_data["creator"] = self.context["request"].user
return super().create(validated_data)

def validate(self, data):
queryset = Advertisement.objects.filter(
creator=self.context['request'].user,
status='OPEN'
)
if len(queryset) >= 10:
raise ValidationError("The limit of 10 open advertisements has been reached.")

return data


class FavoriteSerializer(serializers.ModelSerializer):
advertisement = AdvertisementSerializer(read_only=True)

class Meta:
model = Favorite
fields = ('advertisement',)
read_only_fields = ('advertisement',)

def to_representation(self, instance):
if not self.context['request'].user.is_staff:
return super().to_representation(instance)
else:
return {
'user': instance.user.id,
'advertisement': instance.advertisement.id
}

def create(self, validated_data):
user = self.context['request'].user
adv_id = self.context['request'].query_params.get('adv')
advertisement = get_object_or_404(Advertisement, id=adv_id)
favorite, created = Favorite.objects.get_or_create(user=user, advertisement=advertisement)
if created:
return favorite
else:
raise serializers.ValidationError('Advertisement is already in favorites.')

Loading

0 comments on commit 63e121b

Please sign in to comment.