├── ads ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_urls.py │ ├── test_models.py │ └── test_views.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── pagination.py ├── apps.py ├── urls.py ├── permissions.py ├── admin.py ├── serializers.py ├── models.py └── views.py ├── wall ├── __init__.py ├── settings │ ├── __init__.py │ ├── docker.py │ ├── dev.py │ ├── prod.py │ └── base.py ├── asgi.py ├── wsgi.py └── urls.py ├── accounts ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_urls.py │ ├── test_models.py │ └── test_views.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── serializers.py ├── apps.py ├── permissions.py ├── urls.py ├── models.py ├── views.py ├── admin.py ├── managers.py └── forms.py ├── requirements.txt ├── Dockerfile ├── docker-compose.yaml ├── manage.py ├── nginx.conf ├── LICENSE ├── README.md ├── locale └── fa_IR │ └── LC_MESSAGES │ └── django.po └── .gitignore /ads/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wall/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ads/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ads/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wall/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirhamiri/wall/HEAD/requirements.txt -------------------------------------------------------------------------------- /ads/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | 4 | class StandardResultsSetPagination(PageNumberPagination): 5 | page_size = 1 6 | page_size_query_param = 'page_size' 7 | max_page_size = 50 8 | 9 | -------------------------------------------------------------------------------- /ads/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | class AdsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ads' 7 | verbose_name = _('ads') 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | EXPOSE 8000 3 | WORKDIR /source 4 | 5 | COPY requirements.txt /source 6 | 7 | RUN pip install -U pip 8 | RUN pip install -r requirements.txt 9 | 10 | COPY . /source 11 | CMD ["python","manage.py", "runserver", "0.0.0.0:8000"] -------------------------------------------------------------------------------- /accounts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from accounts.models import User 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = User 9 | fields = ('username',) 10 | 11 | 12 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'accounts' 7 | verbose_name = _('accounts') 8 | -------------------------------------------------------------------------------- /accounts/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsUser(permissions.BasePermission): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | if request.method in permissions.SAFE_METHODS: 8 | return True 9 | return obj.id == request.user.id 10 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework_simplejwt.views import TokenObtainPairView 3 | from . import views 4 | 5 | app_name = 'accounts' 6 | urlpatterns = [ 7 | path('login', TokenObtainPairView.as_view(), name='login'), 8 | path('profile', views.UserView.as_view(),name='profile_view'), 9 | ] -------------------------------------------------------------------------------- /ads/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | app_name = 'ads' 6 | urlpatterns = [ 7 | path('all', views.AdListView.as_view(), name='ad_list'), 8 | path('add', views.AdCreateView.as_view(), name='ad_create'), 9 | path('', views.AdDetailView.as_view(), name='ad_detail') 10 | ] 11 | -------------------------------------------------------------------------------- /ads/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsPublisherOrReadOnly(permissions.BasePermission): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | if request.method in permissions.SAFE_METHODS: 8 | return True 9 | return obj.publisher == request.user 10 | -------------------------------------------------------------------------------- /ads/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | @admin.register(models.Ad) 6 | class AdAdmin(admin.ModelAdmin): 7 | list_display = ('title', 'date_added', 'publisher') 8 | list_filter = ('date_added',) 9 | search_fields = ['title', 'caption'] 10 | list_per_page = 20 11 | readonly_fields = ('date_added',) 12 | 13 | -------------------------------------------------------------------------------- /wall/settings/docker.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | 4 | DEBUG = False 5 | 6 | ALLOWED_HOSTS = ['*'] 7 | 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'NAME': BASE_DIR / 'db.sqlite3', 12 | } 13 | } 14 | 15 | 16 | STATIC_URL = 'static/' 17 | STATIC_ROOT = '/home/static' 18 | MEDIA_URL = 'media/' 19 | MEDIA_ROOT = '/home/media' -------------------------------------------------------------------------------- /wall/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = True 4 | 5 | ALLOWED_HOSTS = [] 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | 'NAME': BASE_DIR / 'db.sqlite3', 11 | } 12 | } 13 | 14 | STATIC_URL = '/static/' 15 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 16 | MEDIA_URL = 'media/' 17 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -------------------------------------------------------------------------------- /ads/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Ad 3 | 4 | 5 | class AdSerializer(serializers.ModelSerializer): 6 | publisher = serializers.ReadOnlyField(source='user.username') 7 | 8 | class Meta: 9 | model = Ad 10 | fields = '__all__' 11 | read_only_fields = ('id', 'date_added', 'is_public') 12 | extra_kwargs = {'image': {'required': False}} 13 | -------------------------------------------------------------------------------- /wall/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for wall project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wall.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /wall/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for wall project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wall.settings.dev') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /accounts/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | from accounts import views 2 | from django.urls import resolve, reverse 3 | from rest_framework.test import APISimpleTestCase 4 | from rest_framework_simplejwt.views import TokenObtainPairView 5 | 6 | class TestUrls(APISimpleTestCase): 7 | def test_login(self): 8 | url = reverse('accounts:login') 9 | self.assertEqual(resolve(url).func.view_class, TokenObtainPairView) 10 | 11 | def test_profile_detail(self): 12 | url = reverse('accounts:profile_view') 13 | self.assertEqual(resolve(url).func.view_class, views.UserView) 14 | 15 | -------------------------------------------------------------------------------- /wall/settings/prod.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from decouple import config 3 | 4 | DEBUG = False 5 | 6 | ALLOWED_HOSTS = ['*'] 7 | 8 | 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.mysql', 12 | 'NAME': config('DB_NAME'), 13 | 'USER': config('DB_USER'), 14 | 'PASSWORD': config('DB_PASSWORD'), 15 | 'HOST': config('DB_HOST'), 16 | 'PORT': 3306 17 | } 18 | } 19 | 20 | STATIC_URL = '/static/' 21 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 22 | STATICFILES_DIRS = [str(BASE_DIR.joinpath('static'))] 23 | MEDIA_URL = 'media/' 24 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | main: 5 | build: . 6 | command: sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 && python manage.py collecstatic" 7 | container_name: main 8 | volumes: 9 | - .:/source/ 10 | ports: 11 | - "8000:8000" 12 | networks: 13 | - wall 14 | restart: always 15 | 16 | nginx: 17 | container_name: nginx 18 | command: nginx -g 'daemon off;' 19 | image: nginx:latest 20 | depends_on: 21 | - main 22 | networks: 23 | - wall 24 | ports: 25 | - "80:80" 26 | restart: always 27 | volumes: 28 | - ./nginx.conf:/etc/nginx/nginx.conf 29 | 30 | networks: 31 | wall: -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wall.settings.dev') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /wall/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.static import static 2 | from django.contrib import admin 3 | from django.urls import path, include 4 | from drf_spectacular.views import SpectacularSwaggerView, SpectacularAPIView 5 | from django.conf import settings 6 | urlpatterns = [ 7 | path('schema/', SpectacularAPIView.as_view(), name='schema'), 8 | path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), 9 | path('admin/', admin.site.urls), 10 | path('api/accounts/', include('accounts.urls')), 11 | path('api/ads/', include('ads.urls')), 12 | ] 13 | 14 | 15 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 16 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 17 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 1; 3 | error_log /var/log/nginx/error.log; 4 | pid /var/run/nginx.pid; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | http { 11 | include /etc/nginx/mime.types; 12 | default_type application/octet-stream; 13 | access_log /var/log/nginx/access.log; 14 | 15 | upstream main { 16 | server 127.0.0.1:8000; 17 | } 18 | 19 | server { 20 | listen 80; 21 | server_name localhost; 22 | charset utf-8; 23 | location /static/ { 24 | alias /home/static; 25 | } 26 | location /media/ { 27 | alias /home/media; 28 | } 29 | location / { 30 | proxy_pass http://main; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /ads/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import resolve, reverse 2 | from rest_framework.test import APITestCase 3 | from ads import views 4 | from ads.models import Ad 5 | 6 | 7 | class TestUrls(APITestCase): 8 | 9 | @classmethod 10 | def setUpTestData(cls): 11 | cls.ad = Ad.objects.create(title='Iphone', caption='Nice') 12 | 13 | 14 | def test_ad_list_url(self): 15 | url = reverse('ads:ad_list') 16 | self.assertEqual(resolve(url).func.view_class, views.AdListView) 17 | 18 | def test_ad_create_url(self): 19 | url = reverse('ads:ad_create') 20 | self.assertEqual(resolve(url).func.view_class, views.AdCreateView) 21 | 22 | def test_ad_detail_url(self): 23 | url = reverse('ads:ad_detail', args=(self.ad.id,)) 24 | self.assertEqual(resolve(url).func.view_class, views.AdDetailView) -------------------------------------------------------------------------------- /accounts/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from rest_framework.test import APITestCase 2 | from django.utils.translation import gettext_lazy as _ 3 | from accounts.models import User 4 | 5 | 6 | class AuthorModelTest(APITestCase): 7 | @classmethod 8 | def setUpTestData(cls): 9 | cls.user = User.objects.create(username='Kami') 10 | 11 | def test_username_label(self): 12 | field_label = self.user._meta.get_field('username').verbose_name 13 | self.assertEqual(field_label, _('username')) 14 | 15 | def test_object_name_is_last_name_comma_first_name(self): 16 | expected_object_name = f'{self.user.username}' 17 | self.assertEqual(str(self.user), expected_object_name) 18 | 19 | def test_str_method(self): 20 | expected_result = self.user.username 21 | self.assertEqual(self.user.__str__(), expected_result) 22 | -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from accounts.managers import UserManager 3 | from django.contrib.auth.models import AbstractBaseUser 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class User(AbstractBaseUser): 8 | username = models.CharField( 9 | verbose_name=_('username'), 10 | max_length=40, 11 | unique=True, 12 | ) 13 | is_active = models.BooleanField(default=True) 14 | is_admin = models.BooleanField(default=False) 15 | objects = UserManager() 16 | USERNAME_FIELD = 'username' 17 | 18 | class Meta: 19 | verbose_name = _('user') 20 | verbose_name_plural = _('users') 21 | 22 | def __str__(self): 23 | return self.username 24 | 25 | def has_perm(self, perm, obj=None): 26 | return True 27 | 28 | def has_module_perms(self, app_label): 29 | return True 30 | 31 | @property 32 | def is_staff(self): 33 | return self.is_admin 34 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from accounts.serializers import UserSerializer 2 | from rest_framework import status 3 | from rest_framework.permissions import IsAuthenticated 4 | from rest_framework.response import Response 5 | from rest_framework.views import APIView 6 | from .models import User 7 | from .permissions import IsUser 8 | 9 | 10 | class UserView(APIView): 11 | permission_classes = [IsAuthenticated] 12 | serializer_class = UserSerializer 13 | 14 | def get(self, request): 15 | user = request.user 16 | serializer = UserSerializer(user) 17 | return Response(serializer.data, status=status.HTTP_200_OK) 18 | 19 | def put(self, request): 20 | user = request.user 21 | serializer = UserSerializer(instance=user, data=request.data) 22 | if serializer.is_valid(): 23 | serializer.save() 24 | return Response(serializer.data, status=status.HTTP_200_OK) 25 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 3 | from django.contrib.auth.models import Group 4 | from accounts.forms import UserChangeForm, UserCreationForm 5 | from accounts.models import User 6 | 7 | 8 | @admin.register(User) 9 | class UserAdmin(BaseUserAdmin): 10 | # The forms to add and change user instances 11 | form = UserChangeForm 12 | add_form = UserCreationForm 13 | 14 | list_display = ('username', 'is_admin') 15 | list_filter = ('is_admin',) 16 | fieldsets = ( 17 | (None, {'fields': ('username', 'password')}), 18 | ('Permissions', {'fields': ('is_admin',)}), 19 | ) 20 | 21 | add_fieldsets = ( 22 | (None, { 23 | 'classes': ('wide',), 24 | 'fields': ('username', 'password1', 'password2'), 25 | }), 26 | ) 27 | search_fields = ('username',) 28 | ordering = ('username',) 29 | filter_horizontal = () 30 | 31 | 32 | admin.site.unregister(Group) -------------------------------------------------------------------------------- /ads/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from rest_framework.test import APITestCase 2 | from django.utils.translation import gettext_lazy as _ 3 | from ads.models import Ad 4 | from accounts.models import User 5 | 6 | 7 | class AuthorModelTest(APITestCase): 8 | @classmethod 9 | def setUpTestData(cls): 10 | cls.user = User.objects.create_user(username='Pedi') 11 | cls.ad = Ad.objects.create(title='Iphone', caption='nice', publisher=cls.user) 12 | 13 | def test_title_label(self): 14 | field_label = self.ad._meta.get_field('title').verbose_name 15 | self.assertEqual(field_label, _('title')) 16 | 17 | def test_caption_label(self): 18 | field_label = self.ad._meta.get_field('caption').verbose_name 19 | self.assertEqual(field_label, _('caption')) 20 | 21 | def test_image_label(self): 22 | field_label = self.ad._meta.get_field('image').verbose_name 23 | self.assertEqual(field_label, _('image')) 24 | 25 | def test_str_method(self): 26 | expected_result = self.ad.title 27 | self.assertEqual(self.ad.__str__(), expected_result) 28 | -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-01-05 22:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='User', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('password', models.CharField(max_length=128, verbose_name='password')), 19 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 20 | ('username', models.CharField(max_length=40, unique=True, verbose_name='username')), 21 | ('is_active', models.BooleanField(default=True)), 22 | ('is_admin', models.BooleanField(default=False)), 23 | ], 24 | options={ 25 | 'verbose_name': 'user', 26 | 'verbose_name_plural': 'users', 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Amirhossein Amiri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ads/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | from django.utils.translation import gettext_lazy as _ 4 | from django.utils.timezone import now 5 | 6 | 7 | class Ad(models.Model): 8 | date_added = models.DateTimeField(_('date published'), 9 | default=now) 10 | title = models.CharField(_('title'), max_length=150) 11 | caption = models.TextField(_('caption')) 12 | image = models.ImageField(_('image'), upload_to='images') 13 | is_public = models.BooleanField(_('is public'),default=True, 14 | help_text=_('Public Ads will be displayed ' 15 | 'in the api views.')) 16 | publisher = models.ForeignKey(settings.AUTH_USER_MODEL,blank=True,null=True, related_name='%(class)s', on_delete=models.CASCADE, 17 | verbose_name=_('publisher')) 18 | 19 | class Meta: 20 | ordering = ('-date_added',) 21 | get_latest_by = 'date_added' 22 | verbose_name = _('ad') 23 | verbose_name_plural = _('ads') 24 | 25 | def __str__(self): 26 | return self.title 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wall 2 | ## a real world open source example of django-rest-framework (DRF) 3 | ### some features: 4 | - Compliance with the principles of test writing DRF 5 | - Compliance with the principles of clean coding 6 | - Dockerized 7 | - Using the nginx web server 8 | - Documented and visualized by Swagger 9 | #### Wall is a Django project based on DRF to share advertisements 10 | #### If you want to get a good understanding of API and DRF, fork the project and participate in its development. 11 | - In terminal: `git clone https://github.com/amirhamiri/wall` 12 | - cd `/wall` Where the manage.py is 13 | - In terminal: `python -m venv venv` 14 | - activate your venv: in windows `cd venv\scripts\activate` in linux: `venv/bin/activate` 15 | - Run `pip install requirements.txt` 16 | - Run `python manage.py collectstatic` 17 | - Run `python manage.py runserver --settings=wall.settings.dev` 18 | - Visit http://127.0.0.1:8000/swagger to watch the api documentation 19 | ## Run project with docker 20 | make sure you`ve installed docker 21 | - In terminal: `git clone https://github.com/amirhamiri/wall` 22 | - cd `/wall` Where the docker-compose.yaml is 23 | - In terminal: `docker-compose up -d` 24 | - Visit http://127.0.0.1:8000/swagger to watch the api documentation 25 | that`s it... -------------------------------------------------------------------------------- /accounts/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | 6 | class BaseUserManager(models.Manager): 7 | @classmethod 8 | def normalize_username(cls, username): 9 | """ 10 | Normalize the username by lowercasing it. 11 | """ 12 | username = username or "" 13 | if len(username) < 4: 14 | raise ValueError(_('username must have at least 4 characters')) 15 | return username.lower() 16 | 17 | def get_by_natural_key(self, username): 18 | return self.get(**{self.model.USERNAME_FIELD: username}) 19 | 20 | 21 | class UserManager(BaseUserManager): 22 | 23 | def create_user(self, username, password=None): 24 | """ 25 | Creates and saves a User with the given username and password. 26 | """ 27 | if not username: 28 | raise ValueError(_('Users must have a username')) 29 | 30 | user = self.model( 31 | username=self.normalize_username(username) 32 | ) 33 | 34 | user.set_password(password) 35 | user.save(using=self._db) 36 | return user 37 | 38 | def create_superuser(self, username, password=None): 39 | """ 40 | Creates and saves a superuser with the given username and password. 41 | """ 42 | user = self.create_user( 43 | username, 44 | password=password, 45 | ) 46 | user.is_admin = True 47 | user.save(using=self._db) 48 | return user 49 | -------------------------------------------------------------------------------- /ads/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-01-05 22:00 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Ad', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('date_added', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date published')), 23 | ('title', models.CharField(max_length=150, verbose_name='title')), 24 | ('caption', models.TextField(verbose_name='caption')), 25 | ('image', models.ImageField(upload_to='images', verbose_name='image')), 26 | ('is_public', models.BooleanField(default=True, help_text='Public Ads will be displayed in the api views.', verbose_name='is public')), 27 | ('publisher', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s', to=settings.AUTH_USER_MODEL, verbose_name='publisher')), 28 | ], 29 | options={ 30 | 'verbose_name': 'ad', 31 | 'verbose_name_plural': 'ads', 32 | 'ordering': ('-date_added',), 33 | 'get_latest_by': 'date_added', 34 | }, 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 3 | from django.core.exceptions import ValidationError 4 | from .models import User 5 | 6 | 7 | class UserCreationForm(forms.ModelForm): 8 | """A form for creating new users. Includes all the required 9 | fields, plus a repeated password.""" 10 | password1 = forms.CharField(label='Password', widget=forms.PasswordInput) 11 | password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) 12 | 13 | class Meta: 14 | model = User 15 | fields = ('username',) 16 | 17 | def clean_password2(self): 18 | # Check that the two password entries match 19 | password1 = self.cleaned_data.get("password1") 20 | password2 = self.cleaned_data.get("password2") 21 | if password1 and password2 and password1 != password2: 22 | raise ValidationError("Passwords don't match") 23 | return password2 24 | 25 | def clean_username(self): 26 | username = self.cleaned_data.get("username") 27 | if len(username) < 4: 28 | raise ValidationError("username must be at least 4 characters") 29 | return username 30 | 31 | def save(self, commit=True): 32 | # Save the provided password in hashed format 33 | user = super().save(commit=False) 34 | user.set_password(self.cleaned_data["password1"]) 35 | if commit: 36 | user.save() 37 | return user 38 | 39 | 40 | class UserChangeForm(forms.ModelForm): 41 | password = ReadOnlyPasswordHashField() 42 | 43 | class Meta: 44 | model = User 45 | fields = ('username', 'password', 'is_active', 'is_admin') 46 | -------------------------------------------------------------------------------- /locale/fa_IR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-01-05 17:57+0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: .\accounts\apps.py:7 21 | msgid "accounts" 22 | msgstr "حساب های کاربری" 23 | 24 | #: .\accounts\managers.py:14 25 | msgid "username must have at least 4 characters" 26 | msgstr "نام کاربری باید حداقل حاوی چهار نویسه باشد." 27 | 28 | #: .\accounts\managers.py:28 29 | msgid "Users must have a username" 30 | msgstr "کاربران باید نام کاربری داشته باشند." 31 | 32 | #: .\accounts\models.py:9 .\accounts\tests\test_models.py:13 33 | msgid "username" 34 | msgstr "نام کاربری" 35 | 36 | #: .\accounts\models.py:19 37 | msgid "user" 38 | msgstr "کاربر" 39 | 40 | #: .\accounts\models.py:20 41 | msgid "users" 42 | msgstr "کاربران" 43 | 44 | #: .\ads\apps.py:7 .\ads\models.py:23 45 | msgid "ads" 46 | msgstr "آگهی ها" 47 | 48 | #: .\ads\models.py:8 49 | msgid "date published" 50 | msgstr "تاریخ انتشار" 51 | 52 | #: .\ads\models.py:10 .\ads\tests\test_models.py:15 53 | msgid "title" 54 | msgstr "عنوان" 55 | 56 | #: .\ads\models.py:11 .\ads\tests\test_models.py:19 57 | #, fuzzy 58 | #| msgid "caption " 59 | msgid "caption" 60 | msgstr "توضیحات" 61 | 62 | #: .\ads\models.py:12 .\ads\tests\test_models.py:23 63 | msgid "image" 64 | msgstr "تصویر" 65 | 66 | #: .\ads\models.py:13 67 | msgid "is public" 68 | msgstr "منتشر شده" 69 | 70 | #: .\ads\models.py:14 71 | msgid "Public Ads will be displayed in the api views." 72 | msgstr "آگهی های منتشر شده در برنامه قابل مشاهده هستند." 73 | 74 | #: .\ads\models.py:17 75 | msgid "publisher" 76 | msgstr "ناشر آگهی" 77 | 78 | #: .\ads\models.py:22 79 | msgid "ad" 80 | msgstr "آگهی" 81 | -------------------------------------------------------------------------------- /accounts/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.urls import reverse 4 | from rest_framework import status 5 | from rest_framework.test import APITestCase 6 | from accounts.models import User 7 | from rest_framework_simplejwt.tokens import RefreshToken 8 | 9 | 10 | class TestUserView(APITestCase): 11 | 12 | @classmethod 13 | def setUpTestData(cls): 14 | cls.username = 'amir' 15 | cls.password = 'test1234' 16 | cls.user = User.objects.create_user(username='amir', password='test1234') 17 | refresh = RefreshToken.for_user(cls.user) 18 | cls.token = str(refresh.access_token) 19 | 20 | def test_profile_detail_authorized(self): 21 | url = reverse('accounts:profile_view') 22 | response = self.client.get(url, HTTP_AUTHORIZATION=f'Bearer {self.token}') 23 | self.assertEqual(response.status_code, status.HTTP_200_OK) 24 | self.assertTrue(response.data['username']) 25 | self.assertEqual(json.loads(response.content), {'username': 'amir'}) 26 | 27 | def test_profile_detail_unauthorized(self): 28 | url = reverse('accounts:profile_view') 29 | response = self.client.get(url) 30 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 31 | 32 | 33 | def test_profile_update_authorized(self): 34 | url = reverse('accounts:profile_view') 35 | data = { 36 | 'username': 'Karim' 37 | } 38 | response = self.client.put(url, data=data, HTTP_AUTHORIZATION=f'Bearer {self.token}') 39 | self.assertEqual(response.status_code, status.HTTP_200_OK) 40 | 41 | def test_profile_update_unauthorized(self): 42 | url = reverse('accounts:profile_view') 43 | data = { 44 | 'username': 'Karim' 45 | } 46 | response = self.client.put(url, data=data) 47 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 48 | 49 | def test_login(self): 50 | url = reverse('accounts:login') 51 | data = { 52 | 'username': self.username, 53 | 'password': self.password 54 | } 55 | response = self.client.post(url, data=data) 56 | self.assertEqual(response.status_code, status.HTTP_200_OK) 57 | self.failUnless(response.data['access']) 58 | -------------------------------------------------------------------------------- /ads/views.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from drf_spectacular.utils import extend_schema 3 | from rest_framework import status 4 | from rest_framework.parsers import MultiPartParser 5 | from rest_framework.response import Response 6 | from rest_framework.views import APIView 7 | from rest_framework.permissions import IsAuthenticated 8 | from .models import Ad 9 | from .serializers import AdSerializer 10 | from .permissions import IsPublisherOrReadOnly 11 | from .pagination import StandardResultsSetPagination 12 | 13 | 14 | class AdListView(APIView, StandardResultsSetPagination): 15 | permission_classes = [IsPublisherOrReadOnly] 16 | serializer_class = AdSerializer 17 | 18 | def get(self, request): 19 | queryset = Ad.objects.filter(is_public=True) 20 | result = self.paginate_queryset(queryset, request) 21 | serializer = AdSerializer(result, many=True) 22 | return self.get_paginated_response(serializer.data) 23 | 24 | 25 | class AdCreateView(APIView): 26 | permission_classes = [IsAuthenticated, IsPublisherOrReadOnly] 27 | serializer_class = AdSerializer 28 | parser_classes = [MultiPartParser] 29 | 30 | def post(self, request): 31 | serializer = AdSerializer(data=request.data) 32 | if serializer.is_valid(): 33 | serializer.validated_data['publisher'] = self.request.user 34 | serializer.save() 35 | return Response(serializer.data, status=status.HTTP_200_OK) 36 | 37 | 38 | class AdDetailView(APIView): 39 | permission_classes = [IsAuthenticated, IsPublisherOrReadOnly] 40 | serializer_class = AdSerializer 41 | parser_classes = [MultiPartParser] 42 | 43 | def get_object(self, pk): 44 | try: 45 | return Ad.objects.get(pk=pk) 46 | except Ad.DoesNotExist: 47 | raise Http404 48 | 49 | def get(self, request, pk): 50 | ad = self.get_object(pk) 51 | serializer = AdSerializer(instance=ad) 52 | return Response(serializer.data, status=status.HTTP_200_OK) 53 | 54 | def put(self, request, pk): 55 | ad = self.get_object(pk) 56 | serializer = AdSerializer(instance=ad, data=request.data) 57 | if serializer.is_valid(): 58 | serializer.save() 59 | return Response(serializer.data, status=status.HTTP_200_OK) 60 | 61 | def delete(self, request, pk): 62 | ad = self.get_object(pk) 63 | ad.delete() 64 | return Response(status=status.HTTP_204_NO_CONTENT) 65 | 66 | 67 | -------------------------------------------------------------------------------- /wall/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | from pathlib import Path 4 | from decouple import config 5 | from django.core.management.utils import get_random_secret_key 6 | 7 | # generating and printing the SECRET_KEY 8 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 9 | 10 | 11 | try: 12 | SECRET_KEY = config('SECRET_KEY') 13 | except: 14 | SECRET_KEY = get_random_secret_key() 15 | 16 | 17 | 18 | # Application definition 19 | 20 | INSTALLED_APPS = [ 21 | 'django.contrib.admin', 22 | 'django.contrib.auth', 23 | 'django.contrib.contenttypes', 24 | 'django.contrib.sessions', 25 | 'django.contrib.messages', 26 | 'django.contrib.staticfiles', 27 | 'ads.apps.AdsConfig', 28 | 'accounts.apps.AccountsConfig', 29 | 'rest_framework', 30 | 'drf_spectacular', 31 | 'django_cleanup' 32 | 33 | ] 34 | 35 | MIDDLEWARE = [ 36 | 'django.middleware.security.SecurityMiddleware', 37 | 'django.contrib.sessions.middleware.SessionMiddleware', 38 | 'django.middleware.common.CommonMiddleware', 39 | 'django.middleware.csrf.CsrfViewMiddleware', 40 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 41 | 'django.contrib.messages.middleware.MessageMiddleware', 42 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 43 | ] 44 | 45 | ROOT_URLCONF = 'wall.urls' 46 | 47 | TEMPLATES = [ 48 | { 49 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 50 | 'DIRS': [BASE_DIR / 'templates'] 51 | , 52 | 'APP_DIRS': True, 53 | 'OPTIONS': { 54 | 'context_processors': [ 55 | 'django.template.context_processors.debug', 56 | 'django.template.context_processors.request', 57 | 'django.contrib.auth.context_processors.auth', 58 | 'django.contrib.messages.context_processors.messages', 59 | ], 60 | }, 61 | }, 62 | ] 63 | 64 | WSGI_APPLICATION = 'wall.wsgi.application' 65 | 66 | 67 | 68 | # Password validation 69 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 70 | 71 | AUTH_PASSWORD_VALIDATORS = [ 72 | { 73 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 74 | }, 75 | { 76 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 77 | }, 78 | { 79 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 80 | }, 81 | { 82 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 83 | }, 84 | ] 85 | 86 | # Internationalization 87 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 88 | 89 | LANGUAGE_CODE = 'fa-ir' 90 | 91 | TIME_ZONE = 'UTC' 92 | 93 | USE_I18N = True 94 | 95 | USE_TZ = True 96 | 97 | # Static files (CSS, JavaScript, Images) 98 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 99 | 100 | 101 | # Default primary key field type 102 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 103 | 104 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 105 | 106 | AUTH_USER_MODEL = 'accounts.User' 107 | 108 | REST_FRAMEWORK = { 109 | 'DEFAULT_RENDERER_CLASSES': [ 110 | 'rest_framework.renderers.JSONRenderer', 111 | 'rest_framework.renderers.BrowsableAPIRenderer', 112 | 113 | ], 114 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 115 | 116 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 117 | 118 | ), 119 | 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 120 | } 121 | SIMPLE_JWT = { 122 | 'ACCESS_TOKEN_LIFETIME': timedelta(days=5000), 123 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=5000), 124 | } 125 | SPECTACULAR_SETTINGS = { 126 | 'TITLE': 'wall API', 127 | 'DESCRIPTION': 'an API doc for wall project', 128 | 'VERSION': '1.0.0', 129 | 'SERVE_INCLUDE_SCHEMA': False, 130 | 'COMPONENT_SPLIT_REQUEST': True 131 | # OTHER SETTINGS 132 | } 133 | 134 | LOCALE_PATHS = os.path.join(BASE_DIR, 'locale/'), 135 | -------------------------------------------------------------------------------- /ads/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | from PIL import Image 4 | from django.urls import reverse 5 | from rest_framework import status 6 | from rest_framework.test import APITestCase 7 | from accounts.models import User 8 | from rest_framework_simplejwt.tokens import RefreshToken 9 | 10 | from ads.models import Ad 11 | 12 | 13 | class TestAdListView(APITestCase): 14 | @classmethod 15 | def setUpTestData(cls): 16 | cls.username = 'amir' 17 | cls.password = 'test1234' 18 | cls.user = User.objects.create_user(username='amir', password='test1234') 19 | cls.ad = Ad.objects.create(title='Iphone', caption='Nice') 20 | refresh = RefreshToken.for_user(cls.user) 21 | cls.token = str(refresh.access_token) 22 | 23 | def test_get_ads_list(self): 24 | url = reverse('ads:ad_list') 25 | response = self.client.get(url) 26 | self.assertEqual(response.status_code, status.HTTP_200_OK) 27 | 28 | 29 | class TestAdDetailView(APITestCase): 30 | 31 | @classmethod 32 | def setUpTestData(cls): 33 | cls.username = 'amir' 34 | cls.password = 'test1234' 35 | cls.user = User.objects.create_user(username='amir', password='test1234') 36 | cls.ad = Ad.objects.create(title='Iphone', caption='Nice') 37 | refresh = RefreshToken.for_user(cls.user) 38 | cls.token = str(refresh.access_token) 39 | 40 | def test_get_ad_detail_authorized(self): 41 | url = reverse('ads:ad_detail', args=(self.ad.id,)) 42 | response = self.client.get(url, HTTP_AUTHORIZATION=f'Bearer {self.token}') 43 | self.assertEqual(response.status_code, status.HTTP_200_OK) 44 | self.assertEqual(response.data['title'], self.ad.title) 45 | 46 | def test_get_ad_detail_unauthorized(self): 47 | url = reverse('ads:ad_detail', args=(self.ad.id,)) 48 | response = self.client.get(url) 49 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 50 | 51 | def test_update_ad_authorized(self): 52 | url = reverse('ads:ad_detail', args=(self.ad.id,)) 53 | new_data = { 54 | 'title': 'Fan', 55 | 'caption': 'Nice' 56 | } 57 | response = self.client.put(url, data=new_data, HTTP_AUTHORIZATION=f'Bearer {self.token}') 58 | self.assertEqual(response.status_code, status.HTTP_200_OK) 59 | self.assertEqual(response.data['title'], new_data['title']) 60 | 61 | def test_update_ad_unauthorized(self): 62 | url = reverse('ads:ad_detail', args=(self.ad.id,)) 63 | new_data = {'title': 'Mic'} 64 | response = self.client.get(url, data=new_data) 65 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 66 | 67 | def test_delete_ad_authorized(self): 68 | url = reverse('ads:ad_detail', args=(self.ad.id,)) 69 | response = self.client.delete(url, HTTP_AUTHORIZATION=f'Bearer {self.token}') 70 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 71 | 72 | def test_delete_ad_unauthorized(self): 73 | url = reverse('ads:ad_detail', args=(self.ad.id,)) 74 | response = self.client.delete(url) 75 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 76 | 77 | 78 | class TestAdCreateView(APITestCase): 79 | @classmethod 80 | def setUpTestData(cls): 81 | cls.username = 'amir' 82 | cls.password = 'test1234' 83 | cls.user = User.objects.create_user(username='amir', password='test1234') 84 | cls.ad = Ad.objects.create(title='Iphone', caption='Nice') 85 | refresh = RefreshToken.for_user(cls.user) 86 | cls.token = str(refresh.access_token) 87 | 88 | def generate_photo_file(self): 89 | file = io.BytesIO() 90 | image = Image.new('RGBA', size=(100, 100), color=(155, 0, 0)) 91 | image.save(file, 'png') 92 | file.name = 'test.png' 93 | file.seek(0) 94 | return file 95 | 96 | def test_create_ad_with_image_authorized(self): 97 | url = reverse('ads:ad_create') 98 | image_file = self.generate_photo_file() 99 | new_data = { 100 | 'title': 'Fan', 101 | 'caption': 'Nice', 102 | 'image': image_file 103 | } 104 | response = self.client.post(url, data=new_data, HTTP_AUTHORIZATION=f'Bearer {self.token}') 105 | print(response.data) 106 | self.assertEqual(response.status_code, status.HTTP_200_OK) 107 | self.assertEqual(response.data['title'], new_data['title']) 108 | 109 | def test_create_ad_without_image_authorized(self): 110 | url = reverse('ads:ad_create') 111 | image_file = self.generate_photo_file() 112 | new_data = { 113 | 'title': 'Fan', 114 | 'caption': 'Nice', 115 | } 116 | response = self.client.post(url, data=new_data, HTTP_AUTHORIZATION=f'Bearer {self.token}') 117 | self.assertEqual(response.status_code, status.HTTP_200_OK) 118 | self.assertEqual(response.data['title'], new_data['title']) 119 | 120 | def test_create_ad_without_image_unauthorized(self): 121 | url = reverse('ads:ad_create') 122 | image_file = self.generate_photo_file() 123 | new_data = { 124 | 'title': 'Fan', 125 | 'caption': 'Nice', 126 | } 127 | response = self.client.post(url, data=new_data) 128 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/django 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=django 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | db.sqlite3-journal 12 | media 13 | 14 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 15 | # in your Git repository. Update and uncomment the following line accordingly. 16 | # /staticfiles/ 17 | 18 | ### Django.Python Stack ### 19 | # Byte-compiled / optimized / DLL files 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | share/python-wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | *.py,cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | cover/ 70 | 71 | # Translations 72 | *.mo 73 | 74 | # Django stuff: 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | .pybuilder/ 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | # For a library or package, you might want to ignore these files since the code is 99 | # intended to run in multiple environments; otherwise, check them in: 100 | # .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # poetry 110 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 111 | # This is especially recommended for binary packages to ensure reproducibility, and is more 112 | # commonly ignored for libraries. 113 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 114 | #poetry.lock 115 | 116 | # pdm 117 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 118 | #pdm.lock 119 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 120 | # in version control. 121 | # https://pdm.fming.dev/#use-with-ide 122 | .pdm.toml 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | .idea/ 173 | .idea 174 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 175 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 176 | 177 | # User-specific stuff 178 | .idea/**/workspace.xml 179 | .idea/**/tasks.xml 180 | .idea/**/usage.statistics.xml 181 | .idea/**/dictionaries 182 | .idea/**/shelf 183 | 184 | # AWS User-specific 185 | .idea/**/aws.xml 186 | 187 | # Generated files 188 | .idea/**/contentModel.xml 189 | 190 | # Sensitive or high-churn files 191 | .idea/**/dataSources/ 192 | .idea/**/dataSources.ids 193 | .idea/**/dataSources.local.xml 194 | .idea/**/sqlDataSources.xml 195 | .idea/**/dynamic.xml 196 | .idea/**/uiDesigner.xml 197 | .idea/**/dbnavigator.xml 198 | 199 | # Gradle 200 | .idea/**/gradle.xml 201 | .idea/**/libraries 202 | 203 | # Gradle and Maven with auto-import 204 | # When using Gradle or Maven with auto-import, you should exclude module files, 205 | # since they will be recreated, and may cause churn. Uncomment if using 206 | # auto-import. 207 | # .idea/artifacts 208 | # .idea/compiler.xml 209 | # .idea/jarRepositories.xml 210 | # .idea/modules.xml 211 | # .idea/*.iml 212 | # .idea/modules 213 | # *.iml 214 | # *.ipr 215 | 216 | # CMake 217 | cmake-build-*/ 218 | 219 | # Mongo Explorer plugin 220 | .idea/**/mongoSettings.xml 221 | 222 | # File-based project format 223 | *.iws 224 | 225 | # IntelliJ 226 | out/ 227 | 228 | # mpeltonen/sbt-idea plugin 229 | .idea_modules/ 230 | 231 | # JIRA plugin 232 | atlassian-ide-plugin.xml 233 | 234 | # Cursive Clojure plugin 235 | .idea/replstate.xml 236 | 237 | # SonarLint plugin 238 | .idea/sonarlint/ 239 | 240 | # Crashlytics plugin (for Android Studio and IntelliJ) 241 | com_crashlytics_export_strings.xml 242 | crashlytics.properties 243 | crashlytics-build.properties 244 | fabric.properties 245 | 246 | # Editor-based Rest Client 247 | .idea/httpRequests 248 | 249 | # Android studio 3.1+ serialized cache file 250 | .idea/caches/build_file_checksums.ser 251 | # End of https://www.toptal.com/developers/gitignore/api/django --------------------------------------------------------------------------------