diff --git a/assets/index.js b/assets/index.js index 9ab9a3c..31c3590 100644 --- a/assets/index.js +++ b/assets/index.js @@ -267,6 +267,8 @@ function loadRealtyDetails(realtyId) { // File uploading --------------------------------------- +// TODO: Add file number and overall size limit support + var $fileUploadContainer = $('#fileUploadContainer') @@ -320,6 +322,12 @@ $fileUploadContainer.on('click', '.existing-file-remove-btn', function($evt) { $('input[type="checkbox"].answer-portfolio-select').on('change', function($evt) { var $that = $(this) + + if ($('input[type="checkbox"].answer-portfolio-select:checked').length > 4) { + $that.prop('checked', false) + return + } + var portfId = $that.val() var $container = $('#answer-portfolio-photo-widget-container') diff --git a/projects/forms.py b/projects/forms.py index 90866f6..4086dee 100644 --- a/projects/forms.py +++ b/projects/forms.py @@ -8,7 +8,7 @@ import itertools from .models import Project, ProjectFile, Portfolio, Answer, AnswerMessage, Realty, PortfolioPhoto, Stage from common.models import Location from specializations.models import Specialization -from users.models import User +from users.models import User, Team class ProjectFilterForm(forms.ModelForm): @@ -184,21 +184,37 @@ class ProjectAnswerForm(forms.ModelForm): def __init__(self, *args, **kwargs): self.request = kwargs.pop('request') - answer_as_team = kwargs.pop('answer_as_team', None) # Optional kwarg + self.project = kwargs.pop('project') + self.answer_as_team = kwargs.pop('answer_as_team', None) super().__init__(*args, **kwargs) - if answer_as_team: + if self.answer_as_team: self.fields['secure_deal_only'].label = 'Работаем только по безопасной сделке' - members = self.request.user.team.contractors.all() + try: members = self.request.user.team.contractors.all() + except Team.DoesNotExist: members = () - # portfolios = tuple(itertools.chain(*tuple(contr.portfolios.all() for contr in members))) portfolios = Portfolio.objects.filter(user__pk__in=tuple(m.pk for m in members)) self.fields['portfolios'].queryset = portfolios else: self.fields['secure_deal_only'].label = 'Работаю только по безопасной сделке' self.fields['portfolios'].queryset = self.request.user.portfolios.all() + + if self.project and self.project.price_and_term_required: + self.fields['budget'].required = True + self.fields['currency'].required = True + self.fields['term'].required = True + self.fields['term_type'].required = True + + def clean(self): + if self.answer_as_team: + try: + self.request.user.team + except Team.DoesNotExist: + raise forms.ValidationError('У исполнителя отсутствует команда') + + return super().clean() class ProjectAnswerMessageForm(forms.ModelForm): diff --git a/projects/migrations/0009_auto_20160816_1439.py b/projects/migrations/0009_auto_20160816_1439.py new file mode 100644 index 0000000..f16a231 --- /dev/null +++ b/projects/migrations/0009_auto_20160816_1439.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-08-16 11:39 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0008_auto_20160815_1900'), + ] + + operations = [ + migrations.AlterField( + model_name='answermessage', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), + ), + migrations.AlterField( + model_name='answermessage', + name='object_id', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/projects/migrations/0010_auto_20160816_1942.py b/projects/migrations/0010_auto_20160816_1942.py new file mode 100644 index 0000000..371c6f6 --- /dev/null +++ b/projects/migrations/0010_auto_20160816_1942.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-08-16 16:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0009_auto_20160816_1439'), + ] + + operations = [ + migrations.AlterField( + model_name='answer', + name='budget', + field=models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True), + ), + migrations.AlterField( + model_name='answer', + name='currency', + field=models.CharField(blank=True, choices=[('rur', 'RUR'), ('usd', 'USD'), ('eur', 'EUR')], max_length=5, null=True), + ), + migrations.AlterField( + model_name='answer', + name='term', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='answer', + name='term_type', + field=models.CharField(blank=True, choices=[('project', 'За проект'), ('hour', 'За час'), ('day', 'За день'), ('month', 'За месяц')], max_length=10, null=True), + ), + ] diff --git a/projects/models.py b/projects/models.py index 2abce6c..24ab994 100644 --- a/projects/models.py +++ b/projects/models.py @@ -118,14 +118,14 @@ class ProjectFile(models.Model): class Answer(models.Model): - budget = models.DecimalField(max_digits=10, decimal_places=0) + budget = models.DecimalField(max_digits=10, decimal_places=0, blank=True, null=True) created = models.DateTimeField(default=timezone.now) - currency = models.CharField(max_length=5, choices=CURRENCIES, default='rur') + currency = models.CharField(max_length=5, choices=CURRENCIES, blank=True, null=True) portfolios = models.ManyToManyField('Portfolio', related_name ='answers', blank=True) project = models.ForeignKey(Project, related_name='answers') secure_deal_only = models.BooleanField(default=False) - term = models.IntegerField(default=0) - term_type = models.CharField(max_length=10, choices=TERMS, default='hour') + term = models.IntegerField(blank=True, null=True) + term_type = models.CharField(max_length=10, choices=TERMS, blank=True, null=True) content_type = models.ForeignKey(ContentType, limit_choices_to=Q(app_label='users', model='user') | Q(app_label='users', model='team')) object_id = models.IntegerField() @@ -146,8 +146,14 @@ class AnswerMessage(models.Model): is_sender_customer = models.BooleanField(default=False) text = models.TextField() - content_type = models.ForeignKey(ContentType, limit_choices_to=Q(app_label='users', model='user') | Q(app_label='users', model='team')) - object_id = models.IntegerField() + content_type = models.ForeignKey( + ContentType, + limit_choices_to=Q(app_label='users', model='user') | Q(app_label='users', model='team'), + null=True, + blank=True, + ) + + object_id = models.IntegerField(null=True, blank=True) contractor_or_team = GenericForeignKey('content_type', 'object_id') def __str__(self): diff --git a/projects/templates/project_detail.html b/projects/templates/project_detail.html index 5ea634e..6fbd9a2 100644 --- a/projects/templates/project_detail.html +++ b/projects/templates/project_detail.html @@ -119,25 +119,28 @@
- {% if request.user.is_contractor %} - + {% if request.user.is_contractor and not answer %} + Ответить на проект -
-
-
- -
+ + {% if can_answer_as_team %} +
+
+
+ +
+
-
+ {% endif %} {% elif request.user.is_customer %} Редактировать @@ -303,7 +306,7 @@ {#
#}
-
+ {% csrf_token %} @@ -316,11 +319,17 @@
{% else %} - + {% csrf_token %} +
+
+ {{ form.non_field_errors|safe }} +
+
+
@@ -331,6 +340,7 @@
+

{{ form.currency.errors.as_text }}

{{ form.currency }}
@@ -344,6 +354,7 @@
+

{{ form.term_type.errors.as_text }}

{{ form.term_type }}
@@ -534,7 +545,7 @@
#}
- + {% csrf_token %} diff --git a/projects/urls.py b/projects/urls.py index d219e03..6464d0d 100644 --- a/projects/urls.py +++ b/projects/urls.py @@ -32,7 +32,7 @@ urlpatterns = [ urls.url(r'^(?P\d+)/restore/$', CustomerProjectRestoreView.as_view(), name='customer-project-restore'), urls.url(r'^(?P\d+)/delete/$', CustomerProjectDeleteView.as_view(), name='customer-project-delete'), - urls.url(r'^(?P\d+)/create-answer-message/$', ProjectAnswerCreateMessageView.as_view(), name='create-answer-message'), + urls.url(r'^create-answer-message/(?P\d+)/$', ProjectAnswerCreateMessageView.as_view(), name='create-answer-message'), 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 116be52..98db86a 100644 --- a/projects/views.py +++ b/projects/views.py @@ -1,9 +1,9 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin +from django.core.files.base import ContentFile from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse, reverse_lazy -from django.core.files.base import ContentFile from django.db.models import Q from django.http import HttpResponseForbidden, HttpResponseRedirect, HttpResponse, Http404 from django.shortcuts import render, get_object_or_404, redirect @@ -11,10 +11,7 @@ from django.views.generic import ListView, DetailView, CreateView, DeleteView, V from django.views.generic.base import ContextMixin from pprint import pprint, pformat import json -import pydash as _; - -_.map = _.map_; -_.filter = _.filter_ +import pydash as _; _.map = _.map_; _.filter = _.filter_ import re from .mixins import LastAccessMixin @@ -50,35 +47,28 @@ class ProjectDetailWithAnswerView(BaseMixin, View): if request.user.is_authenticated() and request.user.is_contractor(): project_answers = project.answers.all() - got_answer = None contractor = request.user - answer = contractor.contractor_answers.first() - if answer and answer in project_answers: - got_answer = True - else: - team = None - answer = None - - try: - team = contractor.team - except Team.DoesNotExist: - pass + answer = _.first(_.filter(project_answers, lambda a: a.author == contractor)) + + if not answer: + try: team = contractor.team + except Team.DoesNotExist: team = None if team: - answer = team.answers.first() - - if answer and answer in project_answers: - got_answer = True + answer = _.first(_.filter(project_answers, lambda a: a.author == team)) - if got_answer: - context.update({'answer': answer}) - else: - if request.GET.get('answer_as_team') == 'on': # TODO: Check for actual possibility to answer as a team + context.update({'answer': answer}) + + if not answer: + try: contractor.team; context.update({'can_answer_as_team': True}) + except Team.DoesNotExist: pass + + if request.GET.get('answer_as_team') == 'on': context.update({'answer_as_team': True}) - form = self.form_class(request=request, answer_as_team=True) + form = self.form_class(request=request, answer_as_team=True, project=project) else: - form = self.form_class(request=request) + form = self.form_class(request=request, project=project) context.update({'form': form}) @@ -88,14 +78,15 @@ class ProjectDetailWithAnswerView(BaseMixin, View): if request.user.is_authenticated() and request.user.is_contractor(): context = self.get_context_data(**kwargs) answer_as_team = None + project = get_object_or_404(Project, pk=kwargs.get('pk')) # TODO: Does this work? - if request.POST.get('answer_as_team') == 'on': # TODO: Check for actual possibility to answer as a team + if request.POST.get('answer_as_team') == 'on': answer_as_team = True if answer_as_team: - form = self.form_class(request.POST, request=request, answer_as_team=True) + form = self.form_class(request.POST, request=request, answer_as_team=True, project=project) else: - form = self.form_class(request.POST, request=request) + form = self.form_class(request.POST, request=request, project=project) project = get_object_or_404(Project, pk=kwargs.get('pk')) context.update({'project': project}) @@ -111,10 +102,10 @@ class ProjectDetailWithAnswerView(BaseMixin, View): if len(file.name) <= 255: AnswerFile.objects.create(file=file, name=file.name, answer=answer) - answer_message = AnswerMessage(text=form.cleaned_data.get('text'), is_sender_customer=False) - answer_message.answer = answer - answer_message.contractor_or_team = answer.author - answer_message.save() + message = AnswerMessage(text=form.cleaned_data.get('text'), is_sender_customer=False) + message.answer = answer + message.contractor_or_team = answer.author + message.save() messages.info(request, 'Отклик успешно размещён') redirect_to = request.POST.get('next') @@ -145,12 +136,22 @@ class ProjectAnswerCreateMessageView(BaseMixin, View): form = self.form_class(request.POST, request=request) if form.is_valid(): - # project = form.save(commit=False) - # project.customer = request.user - # project.save() - # form.save_m2m() + message = form.save(commit=False) + + if request.user.is_contractor(): + answer = get_object_or_404(request.user.contractor_answers, pk=kwargs.get('pk')) + message.answer = answer + message.is_sender_customer = False + message.contractor_or_team = request.user + elif request.user.is_customer(): + answer = get_object_or_404(Answer, pk=kwargs.get('pk')) # TODO: Perform additional checks + message.answer = answer + message.is_sender_customer = True + + message.save() + form.save_m2m() - messages.info(request, 'OK') + messages.info(request, 'Сообщение успешно размещено') else: if form.errors: messages.info(request, ( diff --git a/templates/partials/base.html b/templates/partials/base.html index 546267c..4918bd8 100644 --- a/templates/partials/base.html +++ b/templates/partials/base.html @@ -37,7 +37,7 @@ {% endfor %} {% endif %} -
+
{{ request.user }}
{% if request.user.is_authenticated %}