diff --git a/archilance/util.py b/archilance/util.py index adbd3a1..80b72a4 100644 --- a/archilance/util.py +++ b/archilance/util.py @@ -65,8 +65,8 @@ def model_fields(model, width=200): pprint([( f.name, 'Relation? %s' % f.is_relation, - 'Null? %s' % f.null, - 'Blank? %s' % f.blank if not f.is_relation else '(relation)', + 'Null? %s' % getattr(f, 'null', None), + 'Blank? %s' % getattr(f, 'blank', None), ) for f in fields], width=width) diff --git a/projects/forms.py b/projects/forms.py index a6a1222..a94c7cd 100644 --- a/projects/forms.py +++ b/projects/forms.py @@ -136,8 +136,8 @@ class RealtyForm(forms.ModelForm): ) widgets = { - 'construction_type': forms.Select(attrs={'class':'selectpicker'}), - 'building_classification': forms.Select(attrs={'class':'selectpicker'}), + 'construction_type': forms.Select(attrs={'class': 'selectpicker'}), + 'building_classification': forms.Select(attrs={'class': 'selectpicker'}), } def __init__(self, *args, **kwargs): @@ -158,19 +158,13 @@ class PortfolioForm(forms.ModelForm): fields = '__all__' widgets = { - 'construction_type': forms.Select(attrs={'class':'selectpicker'}), - 'building_classification': forms.Select(attrs={'class':'selectpicker'}), - 'currency': forms.Select(attrs={'class':'selectpicker'}), - 'term_type': forms.Select(attrs={'class':'selectpicker'}), + 'construction_type': forms.Select(attrs={'class': 'selectpicker'}), + 'building_classification': forms.Select(attrs={'class': 'selectpicker'}), + 'currency': forms.Select(attrs={'class': 'selectpicker'}), + 'term_type': forms.Select(attrs={'class': 'selectpicker'}), } class ContractorProjectAnswerForm(forms.ModelForm): - # def __init__(self, *args, **kwargs): - # # import code; code.interact(local=dict(globals(), **locals())) - # self.project_id = kwargs.pop('project_id') - # super().__init__(*args, **kwargs) - # self.fields["project"].initial = self.project_id - class Meta: model = Answer @@ -183,8 +177,8 @@ class ContractorProjectAnswerForm(forms.ModelForm): ) widgets = { - 'currency': forms.Select(attrs={'class':'selectpicker'}), - 'term_type': forms.Select(attrs={'class':'selectpicker'}), + 'currency': forms.Select(attrs={'class': 'selectpicker'}), + 'term_type': forms.Select(attrs={'class': 'selectpicker'}), } diff --git a/projects/migrations/0009_auto_20160802_1414.py b/projects/migrations/0009_auto_20160802_1414.py new file mode 100644 index 0000000..e5c8983 --- /dev/null +++ b/projects/migrations/0009_auto_20160802_1414.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-08-02 11:14 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('projects', '0008_merge'), + ] + + operations = [ + migrations.AlterModelOptions( + name='answer', + options={'ordering': ('-created',), 'verbose_name': 'Отклик на проект', 'verbose_name_plural': 'Отклики на проекты'}, + ), + migrations.RemoveField( + model_name='answer', + name='contractor', + ), + migrations.AddField( + model_name='answer', + name='content_type', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), + preserve_default=False, + ), + migrations.AddField( + model_name='answer', + name='object_id', + field=models.IntegerField(default=None), + preserve_default=False, + ), + migrations.AlterField( + model_name='stage', + name='created', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/projects/migrations/0010_answerfiles.py b/projects/migrations/0010_answerfiles.py new file mode 100644 index 0000000..db3a9d4 --- /dev/null +++ b/projects/migrations/0010_answerfiles.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-08-02 13:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0009_auto_20160802_1414'), + ] + + operations = [ + migrations.CreateModel( + name='AnswerFiles', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('file', models.FileField(upload_to='projects/answer_files')), + ('answer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='projects.Answer')), + ], + options={ + 'verbose_name_plural': 'Файлы для откликов', + 'verbose_name': 'Файл для отклика', + }, + ), + ] diff --git a/projects/migrations/0011_auto_20160802_1653.py b/projects/migrations/0011_auto_20160802_1653.py new file mode 100644 index 0000000..a2ea21e --- /dev/null +++ b/projects/migrations/0011_auto_20160802_1653.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-08-02 13:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0010_answerfiles'), + ] + + operations = [ + migrations.AddField( + model_name='answer', + name='portfolios', + field=models.ManyToManyField(blank=True, related_name='answers', to='projects.Portfolio'), + ), + migrations.AddField( + model_name='answer', + name='secure_deal_only', + field=models.BooleanField(default=False), + ), + ] diff --git a/projects/models.py b/projects/models.py index a296928..44159b5 100644 --- a/projects/models.py +++ b/projects/models.py @@ -1,7 +1,9 @@ -from mptt.models import TreeForeignKey -from datetime import datetime +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.db import models +from django.db.models import Q from django.utils import timezone +from mptt.models import TreeForeignKey from users.models import User, Team from specializations.models import Specialization @@ -117,23 +119,41 @@ class ProjectFile(models.Model): class Answer(models.Model): budget = models.DecimalField(max_digits=10, decimal_places=0) - contractor = models.ForeignKey(User, related_name='answers') created = models.DateTimeField(default=timezone.now) currency = models.CharField(max_length=5, choices=CURRENCIES, default='rur') + 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') 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() + author = GenericForeignKey('content_type', 'object_id') + def __str__(self): return self.text class Meta: - verbose_name = 'Ответ к проекту' - verbose_name_plural = 'Ответы к проектам' + verbose_name = 'Отклик на проект' + verbose_name_plural = 'Отклики на проекты' ordering = ('-created',) +class AnswerFiles(models.Model): + answer = models.ForeignKey(Answer, related_name='files', blank=True, null=True) + name = models.CharField(max_length=255) + file = models.FileField(upload_to='projects/answer_files') + + class Meta: + verbose_name = 'Файл для отклика' + verbose_name_plural = 'Файлы для откликов' + + def __str__(self): + return self.file and self.file.url or self.pk + + class Order(models.Model): contractor = models.ForeignKey(User, null=True, blank=True, related_name='orders') created = models.DateTimeField(default=timezone.now) diff --git a/projects/templates/customer_project_detail.html b/projects/templates/customer_project_detail.html deleted file mode 100644 index 481dce8..0000000 --- a/projects/templates/customer_project_detail.html +++ /dev/null @@ -1,215 +0,0 @@ -{% extends 'partials/base.html' %} - -{% block content %} - {% include 'partials/header.html' %} - {% load staticfiles %} - {% load humanize %} -
-
-
-

{{ project }}

-
-
-
-
-
- execitor-image -
-

- {{ project.contractor.get_full_name }} [ivanov_petr] -

- - -
-
-
-

- Специализации: -

-
- {{ project.specialization }} -
-
-
-
-
    -
  • - {{ project.created }} -
  • -
  • - {{ project.type_work }} -
  • -
- {% if project.secure_transaction %} -
-
-

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

-
- {% endif %} -
-
-
-
-
-
    -
  • - Местоположение: {{ project.realty.country }}, {{ project.realty.city }} -
  • -
  • - Классификация здания: {{ project.realty.building_classification }} -
  • -
  • - Вид строительства: {{ project.realty.type_construction }} -
  • -
-
-
-

- {{ project.text }} -

-
- -
-
-
-

Исполнители

-
-
-
- - - -
-
-
-
-
-

Сравнить кандидатов

-
-
-
-

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

-
-
- {% for answer in project.answers.all %} -
-
-
- execitor-image -
-

- {{ answer.contractor.get_full_name }} [] -

- -
Свободен
-
-
- -
-
-

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

-
-
-
-

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

-

- Срок: {{ answer.term }} {{ answer.term_type }} -

-

Опубликован: {{ answer.created }}

-
- -
-
-
-
-
-
-
-
-
-
-{#
#} -{#
#} -{#

#} -{# Иванов Петр Иванович#} -{#

#} -{# #} -{# 13.0.2016 / 21:05#} -{# #} -{#
#} -{# #} -{# #} -{# #} -{# #} -{# #} -{#
#} -{#

Lorem ipsum dolor sit amet

#} -{#
#} -{#
#} -
-
- {% endfor %} -
- -
- {% include 'partials/pagination.html' %} -
- - {% include 'partials/footer.html' %} -
-
-{% endblock %} - - - diff --git a/projects/templates/project_detail.html b/projects/templates/project_detail.html index 5808868..ffa3556 100644 --- a/projects/templates/project_detail.html +++ b/projects/templates/project_detail.html @@ -9,37 +9,62 @@
-

{{ project }}

+

{{ project.name }}

+
-
-

{{ project.budget|intcomma }}

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

{{ project.budget|intcomma }}

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

@@ -56,24 +81,28 @@

  • - 13.0.2016 + {{ project.created }}
  • - Техническое сопровождение + {{ project.get_work_type_display }}
-
-
-

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

-
+ + {% if project.cro %} +
+
+

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

+
+ {% endif %}
+
  • - Местоположение: + Местоположение: {{ project.realty.country }}, {{ project.realty.city }}
  • Классификация здания: {{ project.realty.building_classification }} @@ -89,162 +118,229 @@

- - Ответить на проект - -
-
- -
-
+ {% if request.user.is_contractor %} + + Ответить на проект + +
+
+ +
+
+ {% elif request.user.is_customer %} + + Редактировать + + +
+ {% csrf_token %} + + + + Снять с публикации + +
+ {% endif %}
- - -
- {% csrf_token %} - - - -
-
-
-

Стоимость

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

Стоимость

+ +
+ +
+
+

Бюджет

+ {{ form.currency }} +
+
+ +
- -
-
-

Бюджет

- {{ form.currency }} +
+
+
+

Срок

+ + +
+ +
+
+

Тип срока

+ {{ form.term_type }} +
+
+ +
- -
-
-
-
-
-

Срок

- - +
+
+
+

Текст

+ +
+
- -
-
-

Тип срока

- {{ form.term_type }} + +
+
+
+ {% for p in user.portfolio.all %} +

{{ p }}

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

Текст

- + +
+
+
+ + + + + + + {% elif request.user.is_customer %} + + + + + +
+
+

Исполнители

+
+
+
+ + +
-
- -
-
-
- {% for p in user.portfolio.all %} -

{{ p }}

- {% endfor %} -
+
+
+

Сравнить кандидатов

-
- -
-
+
+

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

- -
-
- {% for answer in project.answers.all %} -
- -
- - {% if answer.contractor.cro %} -
-
-

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

- {% endif %} -
-
-

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

-

- Срок: {{ answer.term }} {{ answer.term_type }} -

-

Опубликован: {{ answer.created }}

-
- -
-{#
#} -{#
#} -{#

#} -{# Иванов Петр Иванович#} -{#

#} -{# #} -{# 13.0.2016 / 21:05#} -{# #} -{#
#} -{# #} -{# #} -{# #} -{# #} -{# #} -{#
#} -{#

#} -{# Text#} -{#

#} -{#
#} -{#
#} -
+
+
+
+
+
+
+
+
+
+
+{#
#} +{#
#} +{#

#} +{# Иванов Петр Иванович#} +{#

#} +{# #} +{# 13.0.2016 / 21:05#} +{# #} +{#
#} +{# #} +{# #} +{# #} +{# #} +{# #} +{#
#} +{#

Lorem ipsum dolor sit amet

#} +{#
#} +{#
#} +
+
+ {% endfor %}
- {% endfor %} + {% endif %}
-
- {% include 'partials/pagination.html' %} -
- + + {% include 'partials/footer.html' %}
diff --git a/projects/urls.py b/projects/urls.py index ceb5f65..5ff98e1 100644 --- a/projects/urls.py +++ b/projects/urls.py @@ -21,19 +21,22 @@ app_name = 'projects' urlpatterns = [ urls.url(r'^$', ProjectFilterView.as_view(), name='project-filter'), + urls.url(r'^(?P\d+)/$', ProjectView.as_view(), name='detail'), urls.url(r'^create/$', CustomerProjectCreateView.as_view(), name='customer-project-create'), - urls.url(r'^(?P\d+)/$', ProjectView.as_view(), name='detail'), urls.url(r'^(?P\d+)/edit/$', CustomerProjectEditView.as_view(), name='customer-project-edit'), - urls.url(r'^(?P\d+)/trash/$', CustomerProjectTrashView.as_view(), name='customer-project-trash'), 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+)/answer/$', ContractorProjectAnswerView.as_view(), name='contractor-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'), + urls.url(r'^candidate/add/(?P(\d+))/(?P(\d+))/$', add_candidate, name='add-candidate'), urls.url(r'^candidate/comparison/(?P\d+)$', ProjectComparisonView.as_view(), name='comparison'), + + urls.url(r'^offerorder/(?P(\d+))/(?P(\d+))/$', OfferOrderView.as_view(), name='offer-order'), + # urls.url(r'^portfolio/create/$', PortfolioCreateView.as_view(), name='portfolio-create'), - urls.url(r'offerorder/(?P(\d+))/(?P(\d+))/$', OfferOrderView.as_view(), name='offer-order'), ] diff --git a/projects/views.py b/projects/views.py index ea9aef7..157bd98 100644 --- a/projects/views.py +++ b/projects/views.py @@ -32,6 +32,25 @@ from .forms import ( ) +class ProjectView(BaseMixin, View): + template_name = 'project_detail.html' + form_class = ContractorProjectAnswerForm + + def get(self, request, *args, **kwargs): + context = self.get_context_data(**_.merge({}, request.GET, kwargs)) + + project = get_object_or_404(Project, pk=kwargs.get('pk')) + context.update({'project': project}) + + # import code; code.interact(local=dict(globals(), **locals())) + + if request.user.is_authenticated() and request.user.is_contractor(): + form = self.form_class() + context.update({'form': form}) + + return render(request, self.template_name, context) + + class ProjectFilterView(BaseMixin, View): template_name = 'project_filter.html' form_class = ProjectFilterForm @@ -144,27 +163,6 @@ class ProjectFilterView(BaseMixin, View): return render(request, self.template_name, context) -class ProjectView(BaseMixin, View): - template_name = 'project_detail.html' - customer_template_name = 'customer_project_detail.html' - form_class = ContractorProjectAnswerForm - - def get(self, request, *args, **kwargs): - project = get_object_or_404(Project, pk=kwargs.get('pk')) - - context = self.get_context_data(**_.merge({}, request.GET, kwargs)) - context.update({'project': project}) - - # import code; code.interact(local=dict(globals(), **locals())) - - if request.user.is_authenticated() and request.user.is_customer(): - return render(request, self.customer_template_name, context) - else: - form = self.form_class() - context.update({'form': form}) - return render(request, self.template_name, context) - - class CustomerProjectCreateView(BaseMixin, View): form_class = CustomerProjectEditForm realty_form = RealtyForm @@ -246,7 +244,7 @@ class CustomerProjectEditView(BaseMixin, View): template_name = 'customer_project_edit.html' def dispatch(self, request, *args, **kwargs): - if request.user.is_authenticated() and request.user.is_customer() and request.user.pk == int(kwargs.get('pk')): + if request.user.is_authenticated() and request.user.is_customer(): return super().dispatch(request, *args, **kwargs) else: return HttpResponseForbidden('403 Forbidden') @@ -345,6 +343,12 @@ class ContractorProjectAnswerView(BaseMixin, View): class CustomerProjectTrashView(View): form_class = CustomerProjectTrashForm + def dispatch(self, request, *args, **kwargs): + if request.user.is_authenticated() and request.user.is_customer(): + return super().dispatch(request, *args, **kwargs) + else: + return HttpResponseForbidden('403 Forbidden') + def post(self, req, *args, **kwargs): if req.user.is_authenticated(): form = self.form_class(_.merge({}, req.POST, kwargs), req=req) diff --git a/users/models.py b/users/models.py index 5bc9853..1191bdf 100644 --- a/users/models.py +++ b/users/models.py @@ -1,8 +1,8 @@ -from mptt.models import TreeForeignKey, TreeManyToManyField -from datetime import datetime +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, AbstractUser, Group, PermissionsMixin +from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.utils import timezone -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, AbstractUser, Group, PermissionsMixin +from mptt.models import TreeForeignKey, TreeManyToManyField from specializations.models import Specialization @@ -117,6 +117,7 @@ class User(AbstractBaseUser, PermissionsMixin): ) avatar = models.ImageField(upload_to='users/avatars/', blank=True) + contractor_answers = GenericRelation('projects.Answer') contractor_resume = models.OneToOneField(ContractorResume, related_name='contractor', blank=True, null=True) contractor_specializations = TreeManyToManyField(Specialization, related_name='contractors', blank=True) contractor_status = models.CharField(default='free', max_length=20, choices=STATUSES) @@ -182,6 +183,7 @@ class User(AbstractBaseUser, PermissionsMixin): class Team(models.Model): + answers = GenericRelation('projects.Answer') created = models.DateTimeField(default=timezone.now) name = models.CharField(max_length=255) owner = models.OneToOneField(User, related_name='team', blank=True, null=True) diff --git a/users/urls.py b/users/urls.py index 28136f3..083a4fb 100755 --- a/users/urls.py +++ b/users/urls.py @@ -21,24 +21,18 @@ app_name = 'users' urlpatterns = [ urls.url(r'^password/', include('password_reset.urls')), + + urls.url(r'^(?P\d+)/edit/$', UserProfileEditView.as_view(), name='user-profile-edit'), + urls.url(r'^(?P\d+)/financial-info/edit/$', UserFinancialInfoEditView.as_view(), name='user-financial-info-edit'), + urls.url(r'^customers/(?P\d+)/$', CustomerProfileOpenProjectsView.as_view(), name='customer-profile-open-projects'), urls.url(r'^customers/(?P\d+)/trashed-projects/$', CustomerProfileTrashedProjectsView.as_view(), name='customer-profile-trashed-projects'), urls.url(r'^customers/(?P\d+)/current-projects/$', CustomerProfileCurrentProjectsView.as_view(), name='customer-profile-current-projects'), urls.url(r'^customers/(?P\d+)/reviews/$', CustomerProfileReviewsView.as_view(), name='customer-profile-reviews'), - urls.url(r'contractors/$', ContractorFilterView.as_view(), name='contractor-filter'), + urls.url(r'^contractors/$', ContractorFilterView.as_view(), name='contractor-filter'), urls.url(r'^contractors/(?P\d+)/$', ContractorProfileDetailView.as_view(), name='contractor-profile'), urls.url(r'^contractor-office/(?P\d+)/$', ContractorOfficeDetailView.as_view(), name='contractor-office'), - # urls.url(r'^profile/$', UserDetailView.as_view(), name='user-detail'), - urls.url(r'^$', UserListView.as_view(), name='users_list'), - urls.url(r'^test/$', send_mail_test), - # urls.url(r'^info$', UserInfoListView.as_view(), name='users_info_list'), - # urls.url(r'^(?P\d+)/$', UserView.as_view(), name='user_view'), - # urls.url(r'contractors/(?P\d+)/edit/$', ContractorProfileEditView.as_view(), name='contractor-profile-edit'), - - urls.url(r'(?P\d+)/edit/$', UserProfileEditView.as_view(), name='user-profile-edit'), - urls.url(r'(?P\d+)/financial-info/edit/$', UserFinancialInfoEditView.as_view(), name='user-financial-info-edit'), - - # urls.url(r'contractors/(?P\d+)/financialinfo/edit/$', UserFinancialInfoEditView.as_view(), name='contractor-financical'), + urls.url(r'^test/$', send_mail_test), ]