-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 63e121b
Showing
39 changed files
with
820 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from django.contrib import admin | ||
|
||
# Register your models here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class AdvertisementsConfig(AppConfig): | ||
name = 'advertisements' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 added
BIN
+1.72 KB
advertisements/management/commands/__pycache__/create_test_users.cpython-310.pyc
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"')) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
29
advertisements/migrations/0002_alter_advertisement_id_favorite.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | ||
], | ||
), | ||
] |
24 changes: 24 additions & 0 deletions
24
advertisements/migrations/0003_advertisement_draft_alter_favorite_unique_together.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 added
BIN
+1.09 KB
advertisements/migrations/__pycache__/0002_alter_advertisement_id_favorite.cpython-310.pyc
Binary file not shown.
Binary file added
BIN
+891 Bytes
...tions/__pycache__/0003_advertisement_draft_alter_favorite_unique_together.cpython-310.pyc
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.') | ||
|
Oops, something went wrong.