diff --git a/projects/migrations/0014_auto_20160823_1912.py b/projects/migrations/0014_auto_20160823_1912.py new file mode 100644 index 0000000..2011dc6 --- /dev/null +++ b/projects/migrations/0014_auto_20160823_1912.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-08-23 16:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0013_auto_20160819_1735'), + ] + + operations = [ + migrations.AlterModelOptions( + name='project', + options={'ordering': ('-created',), 'verbose_name': 'Проект', 'verbose_name_plural': 'Проекты'}, + ), + migrations.AddField( + model_name='answer', + name='rejected', + field=models.BooleanField(default=False), + ), + ] diff --git a/projects/models.py b/projects/models.py index 4d7ce77..9f4c9b2 100644 --- a/projects/models.py +++ b/projects/models.py @@ -78,7 +78,7 @@ class Project(models.Model, HitCountMixin): ('trashed', 'В корзине'), ('deleted', 'Удален'), ) - + budget = models.DecimalField(max_digits=10, decimal_places=0) budget_by_agreement = models.BooleanField(default=False) created = models.DateTimeField(default=timezone.now) @@ -89,7 +89,6 @@ class Project(models.Model, HitCountMixin): name = models.CharField(max_length=255) price_and_term_required = models.BooleanField(default=False) realty = models.ForeignKey(Realty, blank=True, null=True, related_name='projects') - rejected_answers_count = models.PositiveIntegerField(default=0) specialization = TreeForeignKey(Specialization, related_name='projects') state = models.CharField(default='active', max_length=20, choices=STATES) term = models.IntegerField(default=0) @@ -134,6 +133,7 @@ class Answer(models.Model): term = models.IntegerField(blank=True, null=True) term_type = models.CharField(max_length=10, choices=TERMS, blank=True, null=True) is_archive = models.BooleanField(default=False) + rejected = models.BooleanField(default=False) content_type = models.ForeignKey(ContentType, limit_choices_to=Q(app_label='users', model='user') | Q(app_label='users', model='team')) object_id = models.IntegerField() diff --git a/projects/templates/project_detail.html b/projects/templates/project_detail.html index e4d6105..aec5b6d 100644 --- a/projects/templates/project_detail.html +++ b/projects/templates/project_detail.html @@ -3,27 +3,28 @@ {% load humanize %} {% load thumbnail %} {% load specializtions_tags %} +{% load project_tags %} {% block content %} {% include 'partials/header.html' %}
-
-

{{ project.name }}

-
- -
-
- - {% if request.user.is_contractor %} +
+

{{ project.name }}

+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -164,12 +176,6 @@ {% if request.user.is_contractor %} - - - - - - {% if answer %}
@@ -289,7 +295,7 @@

- {{ message.text }} + {{ message.text|linebreaksbr }}

@@ -448,34 +454,49 @@ - {% elif request.user.is_customer and project in request.user.projects.all %} + + + + + + + + + + + + + + + + + + {% elif request.user.is_customer and project in request.user.projects.all %}

Исполнители

-
- - - +
- - {% if project.answers.exists %} {% if project.candidates.count > 1 %}
@@ -485,164 +506,426 @@
{% endif %} -
-

Новые исполнители

-
- - {% for answer in project.answers.all %} -
-
-
- -
- {% if answer.author.avatar %} - execitor-image - {% else %} - execitor-image +
+
+
+

Новые исполнители

+
+ + {% for answer in project|get_new_answers %} +
+
+
+ +
+ {% if answer.author.avatar %} + execitor-image + {% else %} + execitor-image + {% endif %} +
+
+ +

+ {% if answer.author|class_name == 'User' %} + {{ answer.author.get_full_name }} [{{ answer.author.username }}] + {% elif answer.author|class_name == 'Team' %} + {{ answer.author.name }} + {% endif %} +

+ + + + {% if answer.author|class_name == 'User' %} + {% if answer.author.contractor_status == 'free' %} +
Свободен
+ {% endif %} {% endif %}
- - -

- {% if answer.author|class_name == 'User' %} - {{ answer.author.get_full_name }} [{{ answer.author.username }}] - {% elif answer.author|class_name == 'Team' %} - {{ answer.author.name }} - {% endif %} -

- - - - {% if answer.author|class_name == 'User' %} - {% if answer.author.contractor_status == 'free' %} -
Свободен
- {% endif %} - {% endif %} -
- -
- {% ratings_widget answer.author.pk 'restList2' %} - - {% if answer.author|class_name == 'User' and answer.author.cro %} -
-
-

Есть допуск СРО

+ +
+ {% ratings_widget answer.author.pk 'restList2' %} + + {% if answer.author|class_name == 'User' and answer.author.cro %} +
+
+

Есть допуск СРО

+
+ {% elif answer.author|class_name == 'Team' %} + + +
+
+

Есть допуск СРО

+
+ {% endif %}
- {% elif answer.author|class_name == 'Team' %} - -
-
-

Есть допуск СРО

+
+

+ Цена: + {{ answer.budget|intcomma }} + +

+

+ Срок: {{ answer.term }} {{ answer.get_currency_display }} {{ answer.get_term_type_display|decap }} +

+

Опубликован: {{ answer.created|date:'M d, Y' }}

- {% endif %} -
- -
-

- Цена: - {{ answer.budget|intcomma }} - -

-

- Срок: {{ answer.term }} {{ answer.get_currency_display }} {{ answer.get_term_type_display|decap }} -

-

Опубликован: {{ answer.created|date:'M d, Y' }}

-
- - - -
- {% for portf in answer.portfolios.all %} -
-
-
-
+ +
+ {% if answer.author not in project|get_candidates %} + + Кандидат + + {% endif %} + + + предложить проект + + +
+ {% csrf_token %} + + отказ +
+
+ +
+ {% for portf in answer.portfolios.all %} +
+
+
+
+
+
+
+ {% endfor %} +
+ +
+ {% for message in answer.messages.all %} +
+
+ {% if message.is_sender_customer %} +

+ {{ project.customer.get_full_name }} +

+ {% else %} +

+ {% if answer.author|class_name == 'User' %} + {{ answer.author.get_full_name }} + {% elif answer.author|class_name == 'Team' %} + {{ answer.author.name }} + {% endif %} +

+ {% endif %} + + + {{ message.created }} + + +

+ {{ message.text|linebreaksbr }} +

+
+
+ {% endfor %} + +
+ + +
+
+ {% csrf_token %} + + +
+
+
- {% endfor %} +
- -
- {% for message in answer.messages.all %} -
-
- {% if message.is_sender_customer %} -

- {{ project.customer.get_full_name }} -

- {% else %} -

- {% if answer.author|class_name == 'User' %} - {{ answer.author.get_full_name }} - {% elif answer.author|class_name == 'Team' %} - {{ answer.author.name }} - {% endif %} -

+ {% endfor %} +
+ + + + + +
+
+

Кандидаты

+
+ + {% for answer in project|get_candidate_answers %} +
+
+
+ +
+ {% if answer.author.avatar %} + execitor-image + {% else %} + execitor-image + {% endif %} +
+
+ +

+ {% if answer.author|class_name == 'User' %} + {{ answer.author.get_full_name }} [{{ answer.author.username }}] + {% elif answer.author|class_name == 'Team' %} + {{ answer.author.name }} {% endif %} +

+ + + + {% if answer.author|class_name == 'User' %} + {% if answer.author.contractor_status == 'free' %} +
Свободен
+ {% endif %} + {% endif %} +
+ +
+ {% ratings_widget answer.author.pk 'restList2' %} + + {% if answer.author|class_name == 'User' and answer.author.cro %} +
+
+

Есть допуск СРО

+
+ {% elif answer.author|class_name == 'Team' %} + - - {{ message.created }} - - -

- {{ message.text }} -

-
+
+
+

Есть допуск СРО

+
+ {% endif %}
- {% endfor %} - -
-{# #} -
-
+
+

+ Цена: + {{ answer.budget|intcomma }} + +

+

+ Срок: {{ answer.term }} {{ answer.get_currency_display }} {{ answer.get_term_type_display|decap }} +

+

Опубликован: {{ answer.created|date:'M d, Y' }}

+
+ +
+ + предложить проект + + + {% csrf_token %} - - -
-
+ + отказ
+ +
+ {% for portf in answer.portfolios.all %} +
+
+
+
+
+
+
+ {% endfor %} +
+ +
+ {% for message in answer.messages.all %} +
+
+ {% if message.is_sender_customer %} +

+ {{ project.customer.get_full_name }} +

+ {% else %} +

+ {% if answer.author|class_name == 'User' %} + {{ answer.author.get_full_name }} + {% elif answer.author|class_name == 'Team' %} + {{ answer.author.name }} + {% endif %} +

+ {% endif %} + + + {{ message.created }} + + +

+ {{ message.text|linebreaksbr }} +

+
+
+ {% endfor %} + +
+ + +
+
+ {% csrf_token %} + + +
+
+
+
+
+
+ {% endfor %} +
+ + + + + +
+
+

Отказал

+ + + + {% for answer in project|get_rejected_answers %} +
+
+
+ +
+ {% if answer.author.avatar %} + execitor-image + {% else %} + execitor-image + {% endif %} +
+
+ +

+ {% if answer.author|class_name == 'User' %} + {{ answer.author.get_full_name }} [{{ answer.author.username }}] + {% elif answer.author|class_name == 'Team' %} + {{ answer.author.name }} + {% endif %} +

+ + + + {% if answer.author|class_name == 'User' %} + {% if answer.author.contractor_status == 'free' %} +
Свободен
+ {% endif %} + {% endif %} +
+ +
+ {% ratings_widget answer.author.pk 'restList2' %} + + {% if answer.author|class_name == 'User' and answer.author.cro %} +
+
+

Есть допуск СРО

+
+ {% elif answer.author|class_name == 'Team' %} + + +
+
+

Есть допуск СРО

+
+ {% endif %} +
+ +
+

+ Цена: + {{ answer.budget|intcomma }} + +

+

+ Срок: {{ answer.term }} {{ answer.get_currency_display }} {{ answer.get_term_type_display|decap }} +

+

Опубликован: {{ answer.created|date:'M d, Y' }}

+
+ +
+ {% for portf in answer.portfolios.all %} +
+
+
+
+
+
+
+ {% endfor %} +
+ +
+ {% for message in answer.messages.all %} +
+
+ {% if message.is_sender_customer %} +

+ {{ project.customer.get_full_name }} +

+ {% else %} +

+ {% if answer.author|class_name == 'User' %} + {{ answer.author.get_full_name }} + {% elif answer.author|class_name == 'Team' %} + {{ answer.author.name }} + {% endif %} +

+ {% endif %} + + + {{ message.created }} + + +

+ {{ message.text|linebreaksbr }} +

+
+
+ {% endfor %} +
+
+
+ {% endfor %}
- {% endfor %} +
{% endif %} - - - - - - - - - - {% endif %} - - {% include 'partials/footer.html' %}
diff --git a/projects/templatetags/project_tags.py b/projects/templatetags/project_tags.py new file mode 100644 index 0000000..60af555 --- /dev/null +++ b/projects/templatetags/project_tags.py @@ -0,0 +1,26 @@ +from django import template +from pprint import pprint, pformat + +from archilance import util + + +register = template.Library() + +@register.filter +def get_candidates(project): + return tuple(c.answer.author for c in project.candidates.all()) + +@register.filter +def get_new_answers(project): + return set(project.answers.filter(rejected=False)) - set(c.answer for c in project.candidates.filter(answer__rejected=False)) + +@register.filter +def get_candidate_answers(project): + return tuple(c.answer for c in project.candidates.filter(answer__rejected=False)) + +@register.filter +def get_rejected_answers(project): + return project.answers.filter(rejected=True) + + +# import code; code.interact(local=dict(globals(), **locals())) diff --git a/projects/urls.py b/projects/urls.py index 9044f0e..ac25df9 100644 --- a/projects/urls.py +++ b/projects/urls.py @@ -3,24 +3,23 @@ from django.views.generic import TemplateView from .views import ( add_candidate, + CandidateDeleteView, contractor_portfolio_create, ContractorAnswerArchiveView, ContractorPortfolioTrashView, ContractorPortfolioUpdateView, - ContractorRejectProjectAnswerView, CustomerProjectCreateView, CustomerProjectDeleteView, CustomerProjectEditView, CustomerProjectRestoreView, CustomerProjectTrashView, - CustomerRejectProjectAnswerView, OfferOrderView, ProjectAnswerCreateMessageView, ProjectComparisonView, ProjectDetailWithAnswerView, ProjectFilterView, + RejectProjectAnswerView, sort_candidates, - CandidateDeleteView, ) app_name = 'projects' @@ -38,8 +37,7 @@ urlpatterns = [ urls.url(r'^(?P\d+)/delete/$', CustomerProjectDeleteView.as_view(), name='customer-project-delete'), urls.url(r'^create-answer-message/(?P\d+)/$', ProjectAnswerCreateMessageView.as_view(), name='create-answer-message'), - urls.url(r'^contractor-reject-project-answer/(?P\d+)/$', ContractorRejectProjectAnswerView.as_view(), name='contractor-reject-project-answer'), - urls.url(r'^customer-reject-project-answer/(?P\d+)/$', CustomerRejectProjectAnswerView.as_view(), name='customer-reject-project-answer'), + urls.url(r'^reject-project-answer/(?P\d+)/$', RejectProjectAnswerView.as_view(), name='reject-project-answer'), urls.url(r'^portfolio/create/$', contractor_portfolio_create, name='contractor-portfolio-create'), urls.url(r'^portfolio/(?P\d+)/edit/$', ContractorPortfolioUpdateView.as_view(), name='contractor-portfolio-edit'), diff --git a/projects/views.py b/projects/views.py index 263309f..1753cd4 100644 --- a/projects/views.py +++ b/projects/views.py @@ -1,6 +1,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin +from django.core.exceptions import PermissionDenied from django.core.files.base import ContentFile from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse, reverse_lazy @@ -178,43 +179,24 @@ class ProjectAnswerCreateMessageView(BaseMixin, View): return redirect(redirect_to) -class ContractorRejectProjectAnswerView(BaseMixin, View): +class RejectProjectAnswerView(BaseMixin, View): def dispatch(self, request, *args, **kwargs): - if request.user.is_authenticated() and request.user.is_contractor(): - return super().dispatch(request, *args, **kwargs) - else: - return HttpResponseForbidden('403 Forbidden') - - def post(self, request, *args, **kwargs): - answer = get_object_or_404(request.user.contractor_answers, pk=kwargs.get('pk')) - - answer.delete() - - messages.info(request, 'Вы успешно отказались от проекта') - - redirect_to = request.POST.get('next') - return redirect(redirect_to) - - -class CustomerRejectProjectAnswerView(BaseMixin, View): - def dispatch(self, request, *args, **kwargs): - if request.user.is_authenticated() and request.user.is_customer(): + if request.user.is_authenticated(): return super().dispatch(request, *args, **kwargs) else: - return HttpResponseForbidden('403 Forbidden') + raise PermissionDenied def post(self, request, *args, **kwargs): - answer_id = kwargs.get('pk') - - project = get_object_or_404(request.user.projects, answers__pk=answer_id) - answer = get_object_or_404(project.answers, pk=answer_id) + if request.user.is_contractor(): + answer = get_object_or_404(request.user.contractor_answers, pk=kwargs.get('pk')) + elif request.user.is_customer(): + project = get_object_or_404(request.user.projects, answers__pk=kwargs.get('pk')) + answer = get_object_or_404(project.answers, pk=kwargs.get('pk')) - answer.delete() - - project.rejected_answers_count = F('rejected_answers_count') + 1 - project.save() + answer.rejected = True + answer.save() - messages.info(request, 'Вы успешно отказали пользователю в проекте') + messages.info(request, 'Успешный отказ от проекта') redirect_to = request.POST.get('next') return redirect(redirect_to)