diff --git a/README.md b/README.md
index e8d429a..66eec16 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,35 @@
### Eksikler
-- [ ] Firmaların ilanları eklenecek
-- [ ] Etiketlerin ilanları eklenecek
-- [ ] Ilan gönderme eklenecek
-- [ ] Lokasyona göre ilan arama eklenecek
-- [ ] Search eklenecek (Pozisyon adı, şehir, type/örn: tam zamanlı)
+- [X] Firmaların ilanları eklenecek
+- [X] Etiketlerin ilanları eklenecek
+- [X] Ilan gönderme eklenecek
+- [X] Lokasyona göre ilan arama eklenecek
+- [X] Search eklenecek (Pozisyon adı, şehir, type/örn: tam zamanlı)
+
+### Gereksinimler
+
+- postgresql
+- python 3.6+
+
+### Kurulum
+
+paketlerin yüklenilmesi
+```console
+$ pip install -r requirements.txt
+```
+
+migration oluşturma
+```console
+$ python manage.py makemigrations
+```
+
+migrate
+```console
+$ python manage.py migrate
+```
+
+superuser oluşturma
+```console
+$ python manage.py createsuperuser
+```
\ No newline at end of file
diff --git a/kodilan/mail.py b/kodilan/mail.py
index a2cd156..81ab864 100644
--- a/kodilan/mail.py
+++ b/kodilan/mail.py
@@ -20,7 +20,6 @@ def do_mail(email, title, theme, variables):
pass
-# todo: create post will update
-def send_activation(email, first_name, last_name, token):
+def send_activation(email, company, token):
do_mail(email, "İlan Onay", "activation",
- {'first_name': first_name, 'last_name': last_name, 'code': token})
+ {'company': company, 'code': token})
diff --git a/kodilan/settings.py b/kodilan/settings.py
index e914225..b2d291d 100644
--- a/kodilan/settings.py
+++ b/kodilan/settings.py
@@ -31,17 +31,24 @@
# Application definition
INSTALLED_APPS = [
- 'posts.apps.PostsConfig',
- 'django.contrib.admin',
+ 'material.admin',
+ 'material.admin.default',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ # 3rd party
'rest_framework',
+ 'django_filters',
+ 'corsheaders',
+ # Local
+ 'kodilan',
+ 'posts.apps.PostsConfig'
]
MIDDLEWARE = [
+ 'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -53,18 +60,31 @@
ROOT_URLCONF = 'kodilan.urls'
+
+CORS_ORIGIN_WHITELIST = (
+ 'https://kodilan.com',
+ 'http://kodilan.com',
+ 'http://localhost:3000',
+ 'http://localhost:8000',
+ 'http://localhost:8081',
+ 'http://localhost:8080',
+)
+
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [os.path.join(BASE_DIR, 'templates')]
- ,
+ 'DIRS': [
+ os.path.normpath(os.path.join(BASE_DIR, 'templates')),
+ ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
+ 'django.template.context_processors.media',
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
+ 'django.template.context_processors.request',
],
},
},
@@ -132,5 +152,6 @@
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'kodilan.customs.StandardResultsSetPagination',
+ 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'PAGE_SIZE': 10
}
diff --git a/kodilan/templates/mail/activation.html b/kodilan/templates/mail/activation.html
index 874f42d..e0e7705 100644
--- a/kodilan/templates/mail/activation.html
+++ b/kodilan/templates/mail/activation.html
@@ -1,5 +1,5 @@
-
Merhaba {{ first_name }} {{ last_name }}
+
Merhaba {{ company }}
Kayıt işlemleriniz başarı ile gerçekleştirildi. Aşağıdaki üyelik onay linkine tıklayarak aktivasyon işleminizi
gerçekleştirebilirsiniz.
https://kodilan.com/register/activation?code={{ code }}
diff --git a/kodilan/templates/mail/activation.txt b/kodilan/templates/mail/activation.txt
index d3d6ea0..2a88aa7 100644
--- a/kodilan/templates/mail/activation.txt
+++ b/kodilan/templates/mail/activation.txt
@@ -1,4 +1,4 @@
-Merhaba {{ first_name }} {{ last_name }}
+Merhaba {{ company }}
Kayıt işlemleriniz başarı ile gerçekleştirildi. Aşağıdaki üyelik onay linkine tıklayarak aktivasyon işleminizi gerçekleştirebilirsiniz.
https://kodilan.com/register/activation?code={{ code }}
diff --git a/kodilan/templates/redoc.html b/kodilan/templates/redoc.html
new file mode 100644
index 0000000..c02bbf9
--- /dev/null
+++ b/kodilan/templates/redoc.html
@@ -0,0 +1,21 @@
+
+
+
+
ReDoc
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kodilan/urls.py b/kodilan/urls.py
index 773ea9b..f303c6a 100644
--- a/kodilan/urls.py
+++ b/kodilan/urls.py
@@ -17,14 +17,33 @@
from django.urls import include, path
from rest_framework import routers
from posts import views
+from django.views.generic import TemplateView
+from rest_framework.schemas import get_schema_view
+from rest_framework.renderers import JSONOpenAPIRenderer
router = routers.DefaultRouter()
urlpatterns = [
- path('tags/', views.TagsView.as_view()),
- path('companies/', views.CompaniesView.as_view()),
- path('posts/', views.PostsView.as_view()),
- path('locations/', views.FindLocationAction.as_view()),
+ path('tags', views.TagsView.as_view()),
+ path('companies', views.CompaniesView.as_view()),
+ path('posts', views.PostsView.as_view()),
+ path('posts/slug/
', views.PostView.as_view()),
+ path('posts/activation/', views.ActivatePostView.as_view()),
+ path('locations', views.FindLocationAction.as_view()),
path('', include(router.urls)),
+ path('schema', get_schema_view(
+ title="Kodilan",
+ description="API for all things …",
+ version="0.0.0",
+ renderer_classes=[JSONOpenAPIRenderer]
+ ), name='docs-schema'),
+ path('redoc', TemplateView.as_view(
+ template_name='redoc.html',
+ extra_context={'schema_url': 'docs-schema'}
+ ), name='docs-redoc'),
path('admin/', admin.site.urls),
]
+
+admin.site.site_header = "Kodilan Admin"
+admin.site.site_title = "Kodilan Api"
+admin.site.index_title = "Welcome to Kodilan Researcher Admin"
\ No newline at end of file
diff --git a/openapi-schema.yml b/openapi-schema.yml
new file mode 100644
index 0000000..ed1f93f
--- /dev/null
+++ b/openapi-schema.yml
@@ -0,0 +1,537 @@
+openapi: 3.0.2
+info:
+ title: ''
+ version: ''
+paths:
+ /tags/:
+ get:
+ operationId: listTags
+ description: ''
+ parameters:
+ - name: page
+ required: false
+ in: query
+ description: A page number within the paginated result set.
+ schema:
+ type: integer
+ - name: page_size
+ required: false
+ in: query
+ description: Number of results to return per page.
+ schema:
+ type: integer
+ - name: search
+ required: false
+ in: query
+ description: A search term.
+ schema:
+ type: string
+ - name: ordering
+ required: false
+ in: query
+ description: Which field to use when ordering the results.
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ count:
+ type: integer
+ example: 123
+ next:
+ type: string
+ nullable: true
+ previous:
+ type: string
+ nullable: true
+ results:
+ type: array
+ items:
+ properties:
+ name:
+ type: string
+ maxLength: 190
+ slug:
+ type: string
+ readOnly: true
+ description: ''
+ /companies/:
+ get:
+ operationId: listCompanys
+ description: ''
+ parameters:
+ - name: page
+ required: false
+ in: query
+ description: A page number within the paginated result set.
+ schema:
+ type: integer
+ - name: page_size
+ required: false
+ in: query
+ description: Number of results to return per page.
+ schema:
+ type: integer
+ - name: search
+ required: false
+ in: query
+ description: A search term.
+ schema:
+ type: string
+ - name: ordering
+ required: false
+ in: query
+ description: Which field to use when ordering the results.
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ count:
+ type: integer
+ example: 123
+ next:
+ type: string
+ nullable: true
+ previous:
+ type: string
+ nullable: true
+ results:
+ type: array
+ items:
+ properties:
+ name:
+ type: string
+ maxLength: 190
+ slug:
+ type: string
+ readOnly: true
+ logo:
+ type: string
+ maxLength: 190
+ www:
+ type: string
+ format: uri
+ maxLength: 190
+ pattern: "^(?:[a-z0-9\\.\\-\\+]*)://(?:[^\\s:@/]+(?::[^\\\
+ s:@/]*)?@)?(?:(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(?:\\\
+ .(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}|\\[[0-9a-f:\\\
+ .]+\\]|([a-z\xA1-\uFFFF0-9](?:[a-z\xA1-\uFFFF0-9-]{0,61}[a-z\xA1\
+ -\uFFFF0-9])?(?:\\.(?!-)[a-z\xA1-\uFFFF0-9-]{1,63}(? ' + self.position
def detail(self):
- return f"{self.company.id} > {self.company.name}"
-
- def post_url(self):
- return f"http://localhost:8000/posts/{self.id}"
+ return f"{self.company.id} > {self.company.name}"
\ No newline at end of file
diff --git a/posts/serializers.py b/posts/serializers.py
index 0e7a9ec..c384441 100644
--- a/posts/serializers.py
+++ b/posts/serializers.py
@@ -1,46 +1,112 @@
from .models import Post, Company, Tag
from rest_framework import serializers
+from django.template.defaultfilters import slugify
+from kodilan.mail import send_activation
+from django.db import IntegrityError
+import secrets
+from django.db.models import Q
class CompanySerializer(serializers.HyperlinkedModelSerializer):
+ name = serializers.CharField(required=True, max_length=190)
+ slug = serializers.CharField(read_only=True)
+ email = serializers.EmailField(required=True, max_length=190)
+ logo = serializers.CharField(required=True, max_length=190)
+ www = serializers.URLField(required=True, max_length=190)
+ twitter = serializers.CharField(required=False, max_length=190)
+ linkedin = serializers.CharField(required=False, max_length=190)
+
class Meta:
model = Company
- fields = ['name', 'slug', 'logo', 'www', 'twitter', 'linkedin']
+ fields = ['name', 'slug', 'email', 'logo', 'www', 'twitter', 'linkedin']
class TagSerializer(serializers.ModelSerializer):
+ name = serializers.CharField(required=False, max_length=190)
+ slug = serializers.CharField(read_only=True)
+
class Meta:
model = Tag
fields = ['name', 'slug']
-class CreatePostSerializer(serializers.ModelSerializer):
+class PostSerializer(serializers.ModelSerializer):
+ slug = serializers.CharField(read_only=True)
+ is_featured = serializers.BooleanField(read_only=True)
+ pub_date = serializers.CharField(read_only=True)
+ status = serializers.CharField(read_only=True)
+ position = serializers.CharField(required=True, max_length=190)
description = serializers.CharField(required=True, min_length=4, max_length=100000)
- position = serializers.CharField(required=False, allow_blank=True, max_length=190)
+ apply_url = serializers.URLField(required=True, max_length=190)
+ apply_email = serializers.EmailField(required=True, max_length=190)
+ location = serializers.CharField(required=True, max_length=190)
+ type = serializers.ChoiceField(required=True, choices=Post.TypeEnum)
- apply_url = serializers.CharField(required=False, allow_blank=True, max_length=190)
- apply_email = serializers.CharField(required=False, allow_blank=True, max_length=190)
-
- type = serializers.CharField(required=False, allow_blank=True, max_length=190)
-
- # todo diğer alanlar eklenecek
+ company = CompanySerializer()
+ tags = TagSerializer(many=True, required=False)
+ post_url = serializers.SerializerMethodField()
class Meta:
model = Post
- fields = ['slug', 'position', 'description', 'apply_url', 'apply_email', 'location', 'type', 'status',
- 'is_featured', 'pub_date', 'post_url', 'company', 'tags']
+ fields = ('slug', 'position', 'description', 'apply_url', 'apply_email', 'location', 'type', 'status',
+ 'is_featured', 'pub_date', 'post_url', 'company', 'tags')
+
+ def get_post_url(self, obj):
+ request = self.context.get('request')
+ return request.build_absolute_uri(f"/posts/slug/{obj.slug}")
+
def create(self, validated_data):
- # todo mail gönderimi yapıalcak
- post = Post.objects.create(status=0, **validated_data)
+
+ company_serializer = validated_data['company']
+ company_slug = slugify(company_serializer['name'])
+ try:
+ validated_data['company'] = Company.objects.create(slug=company_slug, **company_serializer)
+ except IntegrityError:
+ validated_data['company'] = Company.objects.filter(
+ Q(name=company_serializer['name']) | Q(email=company_serializer['email'])).first()
+
+ tags_serializer = []
+ if 'tags' in validated_data:
+ tags_serializer = validated_data['tags']
+ del validated_data['tags']
+
+ slug = slugify("%s-%s" % (company_slug, validated_data['position']))
+ token = secrets.token_urlsafe(24)
+ post = Post.objects.create(status=0, is_featured=False, slug=slug, activation_code=token, **validated_data)
+
+ for item in tags_serializer:
+ tags_slug = slugify(item['name'])
+ try:
+ tag = Tag.objects.create(slug=tags_slug, **item)
+ except IntegrityError:
+ tag = Tag.objects.get(name=item['name'])
+
+ post.tags.add(tag)
+
+ send_activation(validated_data['apply_email'], company_serializer['name'], token)
+
return post
-class PostSerializer(serializers.ModelSerializer):
- tags = TagSerializer(many=True)
- company = CompanySerializer()
+class LocationSerializer(serializers.ModelSerializer):
+ location = serializers.CharField(required=True, max_length=190)
class Meta:
model = Post
- fields = ['slug', 'position', 'description', 'apply_url', 'apply_email', 'location', 'type', 'status',
- 'is_featured', 'pub_date', 'post_url', 'company', 'tags']
+ fields = ('location',)
+
+
+class ActivatePostSerializer(serializers.ModelSerializer):
+ status = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Post
+ fields = ('id', 'status')
+
+ def get_status(self, obj):
+ obj.activation_code = None
+ obj.save()
+ # send_activation_status(obj.apply_email, obj.company.name)
+ # if you want you may send activation status success
+ return True
diff --git a/posts/views.py b/posts/views.py
index a9eb004..dd2089b 100644
--- a/posts/views.py
+++ b/posts/views.py
@@ -1,8 +1,6 @@
from .models import Post, Company, Tag
-from .serializers import PostSerializer, CompanySerializer, TagSerializer, CreatePostSerializer
-from rest_framework.response import Response
-from rest_framework.views import APIView
-from rest_framework.generics import ListAPIView, CreateAPIView
+from .serializers import PostSerializer, CompanySerializer, TagSerializer, ActivatePostSerializer, LocationSerializer
+from rest_framework.generics import ListAPIView, ListCreateAPIView, CreateAPIView, RetrieveAPIView
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend
from .filters import PeriodFilterBackend, StatusFilterBackend
@@ -11,24 +9,32 @@
class CompaniesView(ListAPIView):
queryset = Company.objects.all()
serializer_class = CompanySerializer
- filter_backends = [filters.SearchFilter, filters.OrderingFilter]
+ filter_backends = [filters.SearchFilter, filters.OrderingFilter, ]
search_fields = ['name']
ordering_fields = ['id', 'name', 'created_at']
-class PostsView(ListAPIView):
+class PostsView(ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = [StatusFilterBackend, PeriodFilterBackend, DjangoFilterBackend,
filters.SearchFilter, filters.OrderingFilter]
-
- search_fields = ['is_featured', 'location', 'type', 'position', 'tags']
+ filterset_fields = ['tags', 'type', 'position', 'location', 'is_featured']
+ search_fields = ['position', 'description']
ordering_fields = ['pub_date', 'created_at']
-class CreatePostsView(CreateAPIView):
+class PostView(RetrieveAPIView):
+ queryset = Post.objects.all()
+ serializer_class = PostSerializer
+ lookup_field = "slug"
+ filter_backends = [StatusFilterBackend]
+
+
+class ActivatePostView(RetrieveAPIView):
queryset = Post.objects.all()
- serializer_class = CreatePostSerializer
+ serializer_class = ActivatePostSerializer
+ lookup_field = "activation_code"
class TagsView(ListAPIView):
@@ -38,10 +44,7 @@ class TagsView(ListAPIView):
search_fields = ['name']
-class FindLocationAction(APIView):
- def get(self, request):
- locations = Post.objects.all().distinct('location')
- data = {}
- for location in locations:
- data[location.id] = location.location
- return Response(data)
+class FindLocationAction(ListAPIView):
+ queryset = Post.objects.all().values('location').distinct()
+ filter_backends = [StatusFilterBackend]
+ serializer_class = LocationSerializer
diff --git a/requirements.txt b/requirements.txt
index 2883bbf..de07c20 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,4 +10,7 @@ django-filter
django-filters
django-redirect-to-non-www
django-rest-framework
-djangorestframework-simplejwt
\ No newline at end of file
+djangorestframework-simplejwt
+pyyaml
+uritemplate
+django-material-admin
\ No newline at end of file