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