From c2a3053c53eeb5ede5cb0971787b1d3ce492fa43 Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 30 Oct 2017 23:38:44 +0200 Subject: [PATCH 001/385] better UI with flask_bootstrap --- backend/forms/forms.py | 28 ++++---- backend/manage.py | 2 + backend/requirements.txt | 19 +++++ backend/templates/admin_page.html | 48 ++++++++++++- backend/templates/base.html | 29 +------- backend/templates/index.html | 56 +++++++++++++-- backend/templates/login_page.html | 56 ++++++++++++--- backend/templates/user_modify.html | 64 ++++++++++++----- backend/templates/user_page.html | 108 ++++++++++++++++++++--------- backend/views/views.py | 2 +- 10 files changed, 308 insertions(+), 104 deletions(-) create mode 100644 backend/requirements.txt diff --git a/backend/forms/forms.py b/backend/forms/forms.py index ee83514..9951ce7 100644 --- a/backend/forms/forms.py +++ b/backend/forms/forms.py @@ -1,18 +1,22 @@ from flask_wtf import FlaskForm -from wtforms import StringField, IntegerField, DateField, HiddenField, PasswordField -from wtforms.validators import DataRequired, Email, Optional +from wtforms import StringField, IntegerField, DateField, HiddenField, PasswordField, SelectField, SubmitField +from wtforms.validators import DataRequired, Email, Optional, Length + class UserForm(FlaskForm): - """User info modifying form""" - id = HiddenField('id') - name = StringField('name', validators=[DataRequired()]) - alias = StringField('alias', validators=[DataRequired()]) - email = StringField('email', validators=[Email()]) - role_id = IntegerField('role_id', validators=[DataRequired()]) - delete_date = DateField('delete_date',validators=[Optional()]) + """User info modifying form""" + id = HiddenField('id') + name = StringField('name', validators=[DataRequired(), Length(max=20)]) + alias = StringField('alias', validators=[DataRequired()]) + email = StringField('email', validators=[Email()]) + role_id = SelectField('role_id', choices=[( + '1', 'admin'), ('2', 'moderator'), ('3', 'user')], validators=[DataRequired()]) + delete_date = DateField('delete_date', validators=[Optional()]) + submit_button = SubmitField('Save') class LoginForm(FlaskForm): - """Login form""" - email = StringField('login', validators=[Email()]) - password = PasswordField('password', validators=[DataRequired()]) \ No newline at end of file + """Login form""" + email = StringField('login', validators=[Email()]) + password = PasswordField('password', validators=[DataRequired()]) + submit_button = SubmitField('Login') diff --git a/backend/manage.py b/backend/manage.py index a02e2ce..dc5837a 100644 --- a/backend/manage.py +++ b/backend/manage.py @@ -1,6 +1,7 @@ import os from flask import Flask, render_template from flask_sqlalchemy import SQLAlchemy +from flask_bootstrap import Bootstrap config_object = 'config.DevelopmentConfig' @@ -10,6 +11,7 @@ app = Flask(__name__) app.config.from_object(config_object) db = SQLAlchemy(app) +Bootstrap(app) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..2007a17 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,19 @@ +click==6.7 +dominate==2.3.1 +Flask==0.12.2 +Flask-Bootstrap==3.3.7.1 +Flask-SQLAlchemy==2.3.2 +Flask-WTF==0.14.2 +inflect==0.2.5 +itsdangerous==0.24 +Jinja2==2.9.6 +MarkupSafe==1.0 +pep8==1.7.1 +psycopg2==2.7.3.2 +six==1.11.0 +sqlacodegen==1.1.6 +SQLAlchemy==1.1.14 +SQLAlchemy-Utils==0.32.19 +visitor==0.1.3 +Werkzeug==0.12.2 +WTForms==2.1 diff --git a/backend/templates/admin_page.html b/backend/templates/admin_page.html index ee13647..fef6c2d 100644 --- a/backend/templates/admin_page.html +++ b/backend/templates/admin_page.html @@ -1,6 +1,50 @@ {% extends "base.html" %} + +{% block navbar %} +
+
+
+ + + +
+
+
+{% endblock %} + + {% block content %} -

Welcome to the Admin page

+
+
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
+ +
+
+
+

Welcome to the Admin page

+ Work with users +
+
+
+ + - Work with users {% endblock %} diff --git a/backend/templates/base.html b/backend/templates/base.html index a64db7b..91f1fff 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -1,27 +1,4 @@ - - - - CityView - - - - {% with messages = get_flashed_messages() %} - {% if messages %} - - {% endif %} - {% endwith %} -
- Main page -
- {% block content %} - {% endblock %} - - - \ No newline at end of file +{% extends "bootstrap/base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + diff --git a/backend/templates/index.html b/backend/templates/index.html index bc24add..c31bfac 100644 --- a/backend/templates/index.html +++ b/backend/templates/index.html @@ -1,9 +1,51 @@ -{% extends "base.html" %} +{% extends 'bootstrap/base.html' %} + + +{% block title %}CityView{% endblock %} + +{% block navbar %} +
+
+
+ + + +
+
+
+{% endblock %} + + {% block content %} -

Welcome to the CityView

- Login -
- Logout -
- Admin page +
+
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
+ +
+
+
+

Welcome to the CityView

+
+
+
+ {% endblock %} diff --git a/backend/templates/login_page.html b/backend/templates/login_page.html index 51656e0..af1008a 100644 --- a/backend/templates/login_page.html +++ b/backend/templates/login_page.html @@ -1,11 +1,51 @@ {% extends "base.html" %} + + +{% block title %}CityView{% endblock %} + + +{% block navbar %} +
+
+
+ + + +
+
+
+{% endblock %} + + {% block content %} -
- {{ form.csrf_token }} - {{ form.email.label }} {{ form.email(size=40) }} -
- {{ form.password.label }} {{ form.password(size=40) }} -
- -
+
+
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
+ + +
+
+
+

Login page

+ {{ wtf.quick_form(form, action="/login", method="POST") }} +
+
+
+ {% endblock %} \ No newline at end of file diff --git a/backend/templates/user_modify.html b/backend/templates/user_modify.html index 4b5ee56..b5879b5 100644 --- a/backend/templates/user_modify.html +++ b/backend/templates/user_modify.html @@ -1,18 +1,50 @@ -{% extends "base.html" %} +{% extends '/base.html' %} + +{% block title %}CityView - Users view{% endblock %} + +{% block navbar %} +
+
+
+ + + +
+
+
+{% endblock %} + + + {% block content %} -
- {{ form.csrf_token }} - {{ form.hidden_tag() }} - {{ form.name.label }} {{ form.name(size=40) }} -
- {{ form.alias.label }} {{ form.alias(size=20) }} -
- {{ form.email.label }} {{ form.email(size=60) }} -
- {{ form.role_id.label }} {{ form.role_id(size=10) }} -
- {{ form.delete_date.label }} {{ form.delete_date() }} -
- -
+
+
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
+ +
+
+
+

User modifying form

+ {{ wtf.quick_form(form, action="/user_modify", method="POST") }} +
+
+
+ {% endblock %} \ No newline at end of file diff --git a/backend/templates/user_page.html b/backend/templates/user_page.html index b3cbad0..b40d180 100644 --- a/backend/templates/user_page.html +++ b/backend/templates/user_page.html @@ -1,33 +1,77 @@ -{% extends "base.html" %} -{% block content %} -

Welcome to the Admin page

- - - - - - - - - - - - - - {% for user in users %} - - - - - - - - - - - - - {% endfor %} -
IdUserAliasE-mailRoleAvatarDelete date
{{ user[0].id }}{{ user[0].name }}{{ user[0].alias }}{{ user[0].email }}{{ user[1].role }}{{ user[0].avatar }}{{ user[0].delete_date }}EditDelete
- Add new user +{% extends 'base.html' %} + + +{% block title %}CityView - Admin page {% endblock %} + + +{% block navbar %} +
+
+
+ + + +
+
+
{% endblock %} + + +{% block content %} +
+
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
+ +
+
+
+
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + + + + {% endfor %} +
IdUserAliasE-mailRoleAvatarDelete date
{{ user[0].id }}{{ user[0].name }}{{ user[0].alias }}{{ user[0].email }}{{ user[1].role }}{{ user[0].avatar }}{{ user[0].delete_date }}EditDelete
+
+ Add new user +
+
+
+{% endblock %} \ No newline at end of file diff --git a/backend/views/views.py b/backend/views/views.py index f1f031a..f50854f 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -20,7 +20,6 @@ def admin(): flash('Dont have access ...') return redirect(url_for('index')) - @app.route('/user_page') def user_page(): users = db.session.query(User, Role).filter( @@ -113,3 +112,4 @@ def logout(): session.pop('role_id', None) flash("Logout success") return redirect(url_for('index')) + From e8f128cabc23464d598de9b7450ef117ee517f9c Mon Sep 17 00:00:00 2001 From: oksana-pidhurska Date: Tue, 31 Oct 2017 18:33:33 -0400 Subject: [PATCH 002/385] add sorting parameters --- backend/templates/user_page.html | 132 ++++++++++++++++--------------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/backend/templates/user_page.html b/backend/templates/user_page.html index b40d180..61fcb79 100644 --- a/backend/templates/user_page.html +++ b/backend/templates/user_page.html @@ -5,73 +5,81 @@ {% block navbar %} -
-
-
+
+
+
- + -
-
-
+
+
+
{% endblock %} {% block content %} -
-
-
- {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} -
-
-
+
+
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
-
-
-
-
- - - - - - - - - - - - - {% for user in users %} - - - - - - - - - - - - {% endfor %} -
IdUserAliasE-mailRoleAvatarDelete date
{{ user[0].id }}{{ user[0].name }}{{ user[0].alias }}{{ user[0].email }}{{ user[1].role }}{{ user[0].avatar }}{{ user[0].delete_date }}EditDelete
-
- Add new user -
-
-
+
+
+
+
+
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + + + + {% endfor %} +
IdUserAliasE-mailRole + AvatarDelete date +
{{ user[0].id }}{{ user[0].name }}{{ user[0].alias }}{{ user[0].email }}{{ user[1].role }}{{ user[0].avatar }}{{ user[0].delete_date }}EditDelete
+ +
+ +
+ Add new user +
+
+
{% endblock %} \ No newline at end of file From 7ccc1a884f08e4250d84a4611624bd4d57092195 Mon Sep 17 00:00:00 2001 From: oksana-pidhurska Date: Tue, 31 Oct 2017 18:35:29 -0400 Subject: [PATCH 003/385] add sorting in users page --- backend/views/views.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/backend/views/views.py b/backend/views/views.py index f50854f..734d2f2 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -20,10 +20,34 @@ def admin(): flash('Dont have access ...') return redirect(url_for('index')) -@app.route('/user_page') + +@app.route('/user_page', methods=['GET', 'POST']) def user_page(): - users = db.session.query(User, Role).filter( - User.role_id == Role.id).order_by(User.id).all() + data = None + users = None + + if request.method == 'GET': + + users = db.session.query(User, Role).filter(User.role_id == Role.id).order_by(User.id).all() + if request.method == 'POST': + data = request.form.to_dict() + + if data: + if 'delete_date_checkbox' in data and 'role_checkbox' in data: + users = db.session.query(User, Role).filter(User.role_id == Role.id) \ + .order_by(User.role_id, User.delete_date).all() + + elif 'delete_date_checkbox' in data: + users = db.session.query(User, Role).filter(User.role_id == Role.id) \ + .order_by(User.delete_date).all() + + elif 'role_checkbox' in data: + users = db.session.query(User, Role).filter(User.role_id == Role.id) \ + .order_by(User.role_id).all() + else: + users = db.session.query(User, Role).filter(User.role_id == Role.id) \ + .order_by(User.role_id).all() + return render_template('user_page.html', users=users) From bb1823a410f47ce9828ccd72d175c6da818264cb Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 1 Nov 2017 01:13:37 +0200 Subject: [PATCH 004/385] added data validation on edit/add user --- backend/config.py.example | 5 ++- backend/create_database.py | 72 ++++++++++++++++++------------- backend/drop_database.py | 2 +- backend/forms/forms.py | 44 +++++++++++++++---- backend/manage.py | 8 +--- backend/models/users.py | 23 +++++++++- backend/templates/admin_page.html | 4 +- backend/views/views.py | 22 +++------- 8 files changed, 112 insertions(+), 68 deletions(-) diff --git a/backend/config.py.example b/backend/config.py.example index f434c5e..7e30533 100644 --- a/backend/config.py.example +++ b/backend/config.py.example @@ -9,13 +9,14 @@ class Config(object): DEBUG = False SECRET_KEY = 'SECRET_OR_NOT_KEY' + WTF_CSRF_ENABLED = True - WTF_CSRF_SECRET_KEY = 'SECRET_OR_NOT_KEY' + WTF_CSRF_SECRET_KEY = 'SECRET_OR_NOT_KEY' SQLALCHEMY_DATABASE_URI = db_credentials - SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_TRACK_MODIFICATIONS = False class ProductionConfig(Config): DEBUG = False diff --git a/backend/create_database.py b/backend/create_database.py index 95a477f..d85a67c 100644 --- a/backend/create_database.py +++ b/backend/create_database.py @@ -1,11 +1,12 @@ import os -from sqlalchemy_utils.functions.database import create_database, database_exists +from sqlalchemy_utils.functions.database import create_database, \ + database_exists from models.users import Role, User from models.issues import Attachment, Category, Issue, IssueHistory, Status from manage import db from config import Config -#Checking if database exists, and if not -> create it with all tables. +# Checking if database exists, and if not -> create it with all tables. db_credentials = Config.db_credentials @@ -19,7 +20,7 @@ db.session.commit() -#Creating some test data. +# Creating some test data. role = Role(role='admin') role1 = Role(role='moderator') role2 = Role(role='user') @@ -32,37 +33,50 @@ status2 = Status(status="working") status3 = Status(status="closed") -user1 = User(name='Bob', alias='Bobby', email='bob@gmail.com', password='crypto', role_id = '1', avatar=None, delete_date=None) -user2 = User(name='Mark', alias='Marky', email='mark@gmail.com', password='123', role_id = '2', avatar=None, delete_date=None) -user3 = User(name='Maria', alias='Mary', email='maria@gmail.com', password='321', role_id = '3', avatar=None, delete_date=None) - -issue1 = Issue(name='Road accident', user_id='2', category_id='1', location='', description='Car crash ...', - open_date='2017/10/25', close_date=None, delete_date=None) -issue2 = Issue(name='Road accident', user_id='3', category_id='1', location='', description='Bus crash ...', - open_date='2016/01/12', close_date='2016/01/20', delete_date=None) -issue3 = Issue(name='Dog lost', user_id='3', category_id='3', location='', description='Poor puppy is lost', - open_date='2017/09/20', close_date='2017/09/25', delete_date='2017/09/26') +user1 = User(name='Bob', alias='Bobby', email='bob@gmail.com', + password='crypto', role_id='1', avatar=None, delete_date=None) +user2 = User(name='Mark', alias='Marky', email='mark@gmail.com', + password='123', role_id='2', avatar=None, delete_date=None) +user3 = User(name='Maria', alias='Mary', email='maria@gmail.com', + password='321', role_id='3', avatar=None, delete_date=None) + +issue1 = Issue(name='Road accident', user_id='2', category_id='1', location='', + description='Car crash ...', open_date='2017/10/25', + close_date=None, delete_date=None) +issue2 = Issue(name='Road accident', user_id='3', category_id='1', location='', + description='Bus crash ...', open_date='2016/01/12', + close_date='2016/01/20', delete_date=None) +issue3 = Issue(name='Dog lost', user_id='3', category_id='3', location='', + description='Poor puppy is lost', open_date='2017/09/20', + close_date='2017/09/25', delete_date='2017/09/26') attachment1 = Attachment(issue_id='1', image_url='some url1', delete_date=None) -attachment2 = Attachment(issue_id='2', image_url='some url2', delete_date='2016/01/20') -attachment3 = Attachment(issue_id='3', image_url='some url3', delete_date='2017/09/26') - -issueHistory1 = IssueHistory(user_id='1', issue_id='1', status_id='1', transaction_date='2017/10/25', delete_date=None) -issueHistory2 = IssueHistory(user_id='1', issue_id='1', status_id='2', transaction_date='2017/10/27', delete_date=None) -issueHistory3 = IssueHistory(user_id='3', issue_id='3', status_id='1', transaction_date='2017/09/20', delete_date=None) -issueHistory4 = IssueHistory(user_id='3', issue_id='3', status_id='2', transaction_date='2017/09/25', delete_date=None) -issueHistory5 = IssueHistory(user_id='3', issue_id='3', status_id='3', transaction_date='2017/09/26', delete_date=None) - - -#Insert test data into database. -db.session.add_all([role, role1, role2, - category, category1, category2, +attachment2 = Attachment( + issue_id='2', image_url='some url2', delete_date='2016/01/20') +attachment3 = Attachment( + issue_id='3', image_url='some url3', delete_date='2017/09/26') + +issueHistory1 = IssueHistory(user_id='1', issue_id='1', status_id='1', + transaction_date='2017/10/25', delete_date=None) +issueHistory2 = IssueHistory(user_id='1', issue_id='1', status_id='2', + transaction_date='2017/10/27', delete_date=None) +issueHistory3 = IssueHistory(user_id='3', issue_id='3', status_id='1', + transaction_date='2017/09/20', delete_date=None) +issueHistory4 = IssueHistory(user_id='3', issue_id='3', status_id='2', + transaction_date='2017/09/25', delete_date=None) +issueHistory5 = IssueHistory(user_id='3', issue_id='3', status_id='3', + transaction_date='2017/09/26', delete_date=None) + +print user1.data_validataion() +Insert test data into database. +db.session.add_all([role, role1, role2, + category, category1, category2, status1, status2, status3, user1, user2, user3, issue1, issue2, issue3, - issueHistory1, issueHistory2, issueHistory3, issueHistory4, issueHistory5 + issueHistory1, issueHistory2, issueHistory3, + issueHistory4, issueHistory5 ]) db.session.commit() - -print "Ok" \ No newline at end of file +print "Ok" diff --git a/backend/drop_database.py b/backend/drop_database.py index 295dfd0..81ac871 100644 --- a/backend/drop_database.py +++ b/backend/drop_database.py @@ -1,7 +1,7 @@ import os from sqlalchemy_utils.functions.database import drop_database -#Dropping our test base. +# Dropping our test base. drop_database(os.environ['DATABASE_URL']) print 'DB dropped' diff --git a/backend/forms/forms.py b/backend/forms/forms.py index 9951ce7..874ce8f 100644 --- a/backend/forms/forms.py +++ b/backend/forms/forms.py @@ -1,22 +1,48 @@ from flask_wtf import FlaskForm -from wtforms import StringField, IntegerField, DateField, HiddenField, PasswordField, SelectField, SubmitField -from wtforms.validators import DataRequired, Email, Optional, Length +from wtforms import StringField, IntegerField, DateField, \ + HiddenField, PasswordField, SelectField, \ + SubmitField +from wtforms.validators import DataRequired, Email, Optional, Length, Regexp class UserForm(FlaskForm): - """User info modifying form""" + """User info modifying form.""" id = HiddenField('id') - name = StringField('name', validators=[DataRequired(), Length(max=20)]) - alias = StringField('alias', validators=[DataRequired()]) + name = StringField('name', + description=u'Length between 3 and 32 chars', + validators=[ + DataRequired(), + Length(min=3, max=32), + Regexp( + r"^[\w]{3,32}$", + message="""Name must be between 3 and 32 chars + and only letters, numbers and "_" may be used.""") + ]) + alias = StringField('alias', + description=u'Length between 3 and 32 chars', + validators=[ + DataRequired(), + Length(min=3, max=32), + Regexp( + r"^[\w]{3,32}$", + message="""Alias must be between 3 and 32 chars + and only letters, numbers and "_" may be used.""") + ]) email = StringField('email', validators=[Email()]) - role_id = SelectField('role_id', choices=[( - '1', 'admin'), ('2', 'moderator'), ('3', 'user')], validators=[DataRequired()]) - delete_date = DateField('delete_date', validators=[Optional()]) + role_id = SelectField('role_id', choices=[ + ('1', 'admin'), + ('2', 'moderator'), + ('3', 'user')], + validators=[DataRequired()]) + delete_date = DateField('delete_date', + default=None, + description=u'Enter date in YYYY-MM-DD value', + validators=[Optional()], format='%Y-%m-%d') submit_button = SubmitField('Save') class LoginForm(FlaskForm): - """Login form""" + """Login form.""" email = StringField('login', validators=[Email()]) password = PasswordField('password', validators=[DataRequired()]) submit_button = SubmitField('Login') diff --git a/backend/manage.py b/backend/manage.py index 67682a4..4135cff 100644 --- a/backend/manage.py +++ b/backend/manage.py @@ -9,7 +9,7 @@ config_object = 'config.DevelopmentConfig' if 'APP_SETTINGS' in os.environ: - config_object = os.environ['APP_SETTINGS'] + config_object = os.environ['APP_SETTINGS'] app = Flask(__name__) app.config.from_object(config_object) @@ -19,9 +19,3 @@ migrate = Migrate(app, db) manager = Manager(app) manager.add_command('db', MigrateCommand) - - - - - - diff --git a/backend/models/users.py b/backend/models/users.py index 1da2818..37234c0 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -1,5 +1,7 @@ +import re from manage import db + class Role(db.Model): """This class is used for role table in database.""" @@ -23,4 +25,23 @@ class User(db.Model): avatar = db.Column(db.Text) delete_date = db.Column(db.Date) - role = db.relationship(u'Role') \ No newline at end of file + role = db.relationship(u'Role') + + def data_validataion(self): + """Validate the user's data""" + self.name_check = re.match(r'^[\w]{4,32}$', self.name) + self.alias_check = re.match(r'^[\w]{3,32}$', self.alias) + self.email_check = re.match(r'^[^@]+@[^@]+.[^@]+$', self.email) + self.role_id_check = re.match(r'^[1-3]$', self.role_id) + if (self.name_check and self.alias_check and + self.email_check and self.role_id_check): + return True + return False + + def clear_form_data(self, data): + """Remove unnessesary key:value from form data""" + self.data = data + self.data.pop('csrf_token', None) + self.data.pop('submit_button', None) + self.data.pop('id', None) + return self.data diff --git a/backend/templates/admin_page.html b/backend/templates/admin_page.html index fef6c2d..cb7800b 100644 --- a/backend/templates/admin_page.html +++ b/backend/templates/admin_page.html @@ -45,6 +45,4 @@

Welcome to the Admin page

- - -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/backend/views/views.py b/backend/views/views.py index f50854f..70a00a6 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -3,7 +3,6 @@ from manage import app, db from forms.forms import LoginForm, UserForm from models.users import Role, User -from models.issues import Attachment, Category, Issue, IssueHistory, Status @app.route('/') @@ -20,6 +19,7 @@ def admin(): flash('Dont have access ...') return redirect(url_for('index')) + @app.route('/user_page') def user_page(): users = db.session.query(User, Role).filter( @@ -40,29 +40,20 @@ def user_modify(): elif request.method == "POST": - if form.validate_on_submit(): + if (form.validate_on_submit() and + User(**User().clear_form_data(form.data)).data_validataion()): if form.id.data: user = db.session.query(User).get(form.id.data) user.name = form.name.data user.alias = form.alias.data user.email = form.email.data user.role_id = form.role_id.data - - if form.delete_date.data: - user.delete_date = form.delete_date.data - else: - user.delete_date = None + user.delete_date = form.delete_date.data db.session.commit() flash("user modified") else: - newuser = User(name=form.name.data, - alias=form.alias.data, - email=form.email.data, - password=None, - role_id=form.role_id.data, - avatar=None, - delete_date=None) + newuser = User(**User().clear_form_data(form.data)) db.session.add(newuser) db.session.commit() flash("user added") @@ -98,7 +89,7 @@ def login(): session['role_id'] = user.role_id flash('Wellcome %s' % user.name) return redirect(url_for('index')) - else: + else: flash('Incorrect login/password data...') return render_template('login_page.html', form=form) else: @@ -112,4 +103,3 @@ def logout(): session.pop('role_id', None) flash("Logout success") return redirect(url_for('index')) - From 3efa24de12605891921ea4fc128af016c20481eb Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 1 Nov 2017 11:47:45 +0200 Subject: [PATCH 005/385] changed gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7bbc71c..49ba8a7 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,5 @@ ENV/ # mypy .mypy_cache/ +backend/config.py +flask \ No newline at end of file From be5d027a949729652fca2e17e8a50a3c6ea331b5 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 1 Nov 2017 22:08:03 +0200 Subject: [PATCH 006/385] fixed errors, added admin access to routes --- .gitignore | 2 +- backend/app_builder.py | 10 +++ backend/config.py.example | 5 +- backend/create_database.py | 10 +-- backend/drop_database.py | 10 ++- backend/forms/forms.py | 14 ++-- backend/manage.py | 20 ++---- backend/migrations.py | 6 -- backend/models/issues.py | 2 +- backend/models/users.py | 32 ++++----- backend/run.py | 5 -- backend/static/css/style.css | 25 ------- backend/static/js/main.js | 16 ----- backend/templates/admin_page.html | 53 ++++++-------- backend/templates/base.html | 1 - backend/templates/flash-message.html | 17 +++++ backend/templates/index.html | 44 ++++-------- backend/templates/login_page.html | 46 +++++------- backend/templates/user_modify.html | 51 +++++--------- backend/templates/user_page.html | 102 ++++++++++++--------------- backend/views/views.py | 49 ++++++++----- requirements.txt | 1 - 22 files changed, 217 insertions(+), 304 deletions(-) create mode 100644 backend/app_builder.py delete mode 100644 backend/migrations.py delete mode 100644 backend/run.py delete mode 100644 backend/static/css/style.css delete mode 100644 backend/static/js/main.js create mode 100644 backend/templates/flash-message.html diff --git a/.gitignore b/.gitignore index eae55c7..94aaf68 100644 --- a/.gitignore +++ b/.gitignore @@ -100,7 +100,7 @@ ENV/ # mypy .mypy_cache/ -backend/config.py +# virtual env flask #configuration file diff --git a/backend/app_builder.py b/backend/app_builder.py new file mode 100644 index 0000000..220c798 --- /dev/null +++ b/backend/app_builder.py @@ -0,0 +1,10 @@ +import os + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config.from_object('config.DevelopmentConfig') +db = SQLAlchemy(app) + + diff --git a/backend/config.py.example b/backend/config.py.example index 7e30533..3e41d08 100644 --- a/backend/config.py.example +++ b/backend/config.py.example @@ -1,6 +1,4 @@ # Rename this file to config.py and fill db_credentials with your's data. -import os - class Config(object): # Change database credentials to yours. @@ -18,9 +16,10 @@ class Config(object): SQLALCHEMY_TRACK_MODIFICATIONS = False + class ProductionConfig(Config): DEBUG = False class DevelopmentConfig(Config): - DEBUG = True \ No newline at end of file + DEBUG = True diff --git a/backend/create_database.py b/backend/create_database.py index d85a67c..e80b1e0 100644 --- a/backend/create_database.py +++ b/backend/create_database.py @@ -1,9 +1,9 @@ import os -from sqlalchemy_utils.functions.database import create_database, \ - database_exists +from sqlalchemy_utils.functions.database import (create_database, + database_exists) from models.users import Role, User from models.issues import Attachment, Category, Issue, IssueHistory, Status -from manage import db +from app_builder import db from config import Config # Checking if database exists, and if not -> create it with all tables. @@ -67,8 +67,8 @@ issueHistory5 = IssueHistory(user_id='3', issue_id='3', status_id='3', transaction_date='2017/09/26', delete_date=None) -print user1.data_validataion() -Insert test data into database. + +# Insert test data into database. db.session.add_all([role, role1, role2, category, category1, category2, status1, status2, status3, diff --git a/backend/drop_database.py b/backend/drop_database.py index 81ac871..b0d5c2f 100644 --- a/backend/drop_database.py +++ b/backend/drop_database.py @@ -1,7 +1,15 @@ import os + from sqlalchemy_utils.functions.database import drop_database +from config import Config + +db_credentials = Config.db_credentials + +if 'DATABASE_URL' in os.environ: + db_credentials = os.environ['DATABASE_URL'] + # Dropping our test base. -drop_database(os.environ['DATABASE_URL']) +drop_database(db_credentials) print 'DB dropped' diff --git a/backend/forms/forms.py b/backend/forms/forms.py index 874ce8f..21f6545 100644 --- a/backend/forms/forms.py +++ b/backend/forms/forms.py @@ -1,7 +1,7 @@ from flask_wtf import FlaskForm -from wtforms import StringField, IntegerField, DateField, \ - HiddenField, PasswordField, SelectField, \ - SubmitField +from wtforms import (StringField, IntegerField, DateField, + HiddenField, PasswordField, SelectField, + SubmitField) from wtforms.validators import DataRequired, Email, Optional, Length, Regexp @@ -15,8 +15,8 @@ class UserForm(FlaskForm): Length(min=3, max=32), Regexp( r"^[\w]{3,32}$", - message="""Name must be between 3 and 32 chars - and only letters, numbers and "_" may be used.""") + message='Name must be between 3 and 32 chars ' + 'and only letters, numbers and "_" may be used.') ]) alias = StringField('alias', description=u'Length between 3 and 32 chars', @@ -25,8 +25,8 @@ class UserForm(FlaskForm): Length(min=3, max=32), Regexp( r"^[\w]{3,32}$", - message="""Alias must be between 3 and 32 chars - and only letters, numbers and "_" may be used.""") + message='Alias must be between 3 and 32 chars ' + 'and only letters, numbers and "_" may be used.') ]) email = StringField('email', validators=[Email()]) role_id = SelectField('role_id', choices=[ diff --git a/backend/manage.py b/backend/manage.py index 4135cff..77e223c 100644 --- a/backend/manage.py +++ b/backend/manage.py @@ -1,21 +1,11 @@ import os -from flask import Flask, render_template -from flask_migrate import Migrate, MigrateCommand -from flask_script import Manager -from flask_sqlalchemy import SQLAlchemy -from flask_bootstrap import Bootstrap - -config_object = 'config.DevelopmentConfig' +from flask_bootstrap import Bootstrap +from app_builder import app, db -if 'APP_SETTINGS' in os.environ: - config_object = os.environ['APP_SETTINGS'] +from views import views -app = Flask(__name__) -app.config.from_object(config_object) -db = SQLAlchemy(app) Bootstrap(app) -migrate = Migrate(app, db) -manager = Manager(app) -manager.add_command('db', MigrateCommand) +if __name__ == '__main__': + app.run() diff --git a/backend/migrations.py b/backend/migrations.py deleted file mode 100644 index ba3f20e..0000000 --- a/backend/migrations.py +++ /dev/null @@ -1,6 +0,0 @@ -from manage import * -from models import users, issues - - -if __name__ == '__main__': - manager.run() diff --git a/backend/models/issues.py b/backend/models/issues.py index 50c9a28..6eba75e 100644 --- a/backend/models/issues.py +++ b/backend/models/issues.py @@ -1,4 +1,4 @@ -from manage import db +from app_builder import db class Attachment(db.Model): diff --git a/backend/models/users.py b/backend/models/users.py index 37234c0..06d3559 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -1,5 +1,5 @@ -import re -from manage import db +# import re +from app_builder import db class Role(db.Model): @@ -27,21 +27,13 @@ class User(db.Model): role = db.relationship(u'Role') - def data_validataion(self): - """Validate the user's data""" - self.name_check = re.match(r'^[\w]{4,32}$', self.name) - self.alias_check = re.match(r'^[\w]{3,32}$', self.alias) - self.email_check = re.match(r'^[^@]+@[^@]+.[^@]+$', self.email) - self.role_id_check = re.match(r'^[1-3]$', self.role_id) - if (self.name_check and self.alias_check and - self.email_check and self.role_id_check): - return True - return False - - def clear_form_data(self, data): - """Remove unnessesary key:value from form data""" - self.data = data - self.data.pop('csrf_token', None) - self.data.pop('submit_button', None) - self.data.pop('id', None) - return self.data + # def data_validataion(self): + # """Validate the user's data""" + # self.name_check = re.match(r'^[\w]{4,32}$', self.name) + # self.alias_check = re.match(r'^[\w]{3,32}$', self.alias) + # self.email_check = re.match(r'^[^@]+@[^@]+.[^@]+$', self.email) + # self.role_id_check = re.match(r'^[1-3]$', self.role_id) + # if (self.name_check and self.alias_check and + # self.email_check and self.role_id_check): + # return True + # return False diff --git a/backend/run.py b/backend/run.py deleted file mode 100644 index 7ec9792..0000000 --- a/backend/run.py +++ /dev/null @@ -1,5 +0,0 @@ -from manage import app -import views.views - -if __name__ == '__main__': - app.run() diff --git a/backend/static/css/style.css b/backend/static/css/style.css deleted file mode 100644 index 2ff7e4c..0000000 --- a/backend/static/css/style.css +++ /dev/null @@ -1,25 +0,0 @@ -.button { - color: red; - cursor: pointer; - text-decoration: underline; - -} - -.user_table { - border: 2px solid black; - border-collapse: collapse; -} - -.user_table-cell { - border: 1px solid black; - border-collapse: collapse; -} - -.form_user_label { - display: block; - margin-bottom: 20px; -} - -.flash_message { - color: red; -} \ No newline at end of file diff --git a/backend/static/js/main.js b/backend/static/js/main.js deleted file mode 100644 index bb465b8..0000000 --- a/backend/static/js/main.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - - -var btn = document.getElementById('deleteMe'); -var idToDelete = 2; -var jsontoSend = JSON.stringify(idToDelete); - -function show() { - alert(jsontoSend); - var req = new XMLHttpRequest(); - req.open('GET', '/index', true); - req.send(); - window.location.href = '/index'; -} - -btn.addEventListener('click', show); \ No newline at end of file diff --git a/backend/templates/admin_page.html b/backend/templates/admin_page.html index cb7800b..ff31438 100644 --- a/backend/templates/admin_page.html +++ b/backend/templates/admin_page.html @@ -1,48 +1,35 @@ {% extends "base.html" %} {% block navbar %} -
-
-
+
+
+
- + +
-
{% endblock %} {% block content %} -
-
-
- {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} -
-
-
-
-
-
-

Welcome to the Admin page

- Work with users + {% include 'flash-message.html' %} + +
+
+
+

Welcome to the Users management

+ Work with users +
-
- + {% endblock %} \ No newline at end of file diff --git a/backend/templates/base.html b/backend/templates/base.html index 91f1fff..329dae1 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -1,4 +1,3 @@ - {% extends "bootstrap/base.html" %} {% import "bootstrap/wtf.html" as wtf %} diff --git a/backend/templates/flash-message.html b/backend/templates/flash-message.html new file mode 100644 index 0000000..d946e95 --- /dev/null +++ b/backend/templates/flash-message.html @@ -0,0 +1,17 @@ +{% block flash_message %} +
+
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/backend/templates/index.html b/backend/templates/index.html index c31bfac..a1a0237 100644 --- a/backend/templates/index.html +++ b/backend/templates/index.html @@ -1,44 +1,30 @@ {% extends 'bootstrap/base.html' %} - {% block title %}CityView{% endblock %} {% block navbar %} -
-
-
- - +
+
+
+ + +
-
{% endblock %} {% block content %} -
-
-
- {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} -
-
-
+ + {% include 'flash-message.html' %}
diff --git a/backend/templates/login_page.html b/backend/templates/login_page.html index 187071d..ba64ed2 100644 --- a/backend/templates/login_page.html +++ b/backend/templates/login_page.html @@ -5,47 +5,33 @@ {% block navbar %} -
-
-
+
+
+
- + +
-
{% endblock %} {% block content %} -
-
-
- {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} -
-
-
+ {% include 'flash-message.html' %} -
-
-
-

Login page

- {{ wtf.quick_form(form, action="/login", method="POST", button_map={'submit_button' : 'success'}) }} +
+
+
+

Login page

+ {{ wtf.quick_form(form, action="/login", method="POST", button_map={'submit_button' : 'success'}) }} +
-
{% endblock %} \ No newline at end of file diff --git a/backend/templates/user_modify.html b/backend/templates/user_modify.html index cd87643..f45b447 100644 --- a/backend/templates/user_modify.html +++ b/backend/templates/user_modify.html @@ -3,48 +3,35 @@ {% block title %}CityView - Users view{% endblock %} {% block navbar %} -
-
-
- - +
+
+
+ + +
-
{% endblock %} {% block content %} -
-
-
- {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} -
-
-
-
-
-
-

User modifying form

- {{ wtf.quick_form(form, action="/user_modify", method="POST", button_map={'submit_button' : 'success'}) }} + {% include 'flash-message.html' %} + +
+
+
+

User modifying form

+ {{ wtf.quick_form(form, action="/user_modify", method="POST", button_map={'submit_button' : 'success'}) }} +
-
{% endblock %} \ No newline at end of file diff --git a/backend/templates/user_page.html b/backend/templates/user_page.html index b40d180..200cb41 100644 --- a/backend/templates/user_page.html +++ b/backend/templates/user_page.html @@ -5,73 +5,61 @@ {% block navbar %} -
-
-
+
+
+
- + +
-
{% endblock %} {% block content %} -
-
-
- {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} -
-
-
-
-
-
-
- - - - - - - - - - - - - {% for user in users %} - - - - - - - - - - - - {% endfor %} -
IdUserAliasE-mailRoleAvatarDelete date
{{ user[0].id }}{{ user[0].name }}{{ user[0].alias }}{{ user[0].email }}{{ user[1].role }}{{ user[0].avatar }}{{ user[0].delete_date }}EditDelete
+ {% include 'flash-message.html' %} + +
+
+
+
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + + + + {% endfor %} +
IdUserAliasE-mailRoleAvatarDelete date
{{ user[0].id }}{{ user[0].name }}{{ user[0].alias }}{{ user[0].email }}{{ user[1].role }}{{ user[0].avatar }}{{ user[0].delete_date }}EditDelete
+
+ Add new user
- Add new user
-
+ {% endblock %} \ No newline at end of file diff --git a/backend/views/views.py b/backend/views/views.py index 70a00a6..e704f11 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -1,10 +1,32 @@ from datetime import datetime +from functools import wraps + from flask import flash, render_template, redirect, request, session, url_for -from manage import app, db + +from app_builder import app, db from forms.forms import LoginForm, UserForm from models.users import Role, User +def admin_permissions(func): + """Decorator to check admin rights to access some route.""" + @wraps(func) + def wrapper(*args, **kwargs): + if not 'user_id' in session or session['role_id'] != 1: + flash("Don't have access") + return redirect(url_for('index')) + return func(*args, **kwargs) + return wrapper + + +def form_data_clean(data): + """Remove unnessery fields from form.data.""" + data.pop('csrf_token', None) + data.pop('submit_button', None) + data.pop('id', None) + return data + + @app.route('/') @app.route('/index') def index(): @@ -12,15 +34,13 @@ def index(): @app.route('/admin') +@admin_permissions def admin(): - if 'user_id' in session and session['role_id'] == 1: - return render_template('admin_page.html') - else: - flash('Dont have access ...') - return redirect(url_for('index')) + return render_template('admin_page.html') @app.route('/user_page') +@admin_permissions def user_page(): users = db.session.query(User, Role).filter( User.role_id == Role.id).order_by(User.id).all() @@ -28,32 +48,29 @@ def user_page(): @app.route("/user_modify", methods=['GET', 'POST']) +@admin_permissions def user_modify(): form = UserForm(request.form) if 'id' in request.args: - user = db.session.query(User).get(request.args.get('id')) - form = UserForm(obj=user) + chosen_one = db.session.query(User).get(request.args.get('id')) + form = UserForm(obj=chosen_one) if request.method == "GET": return render_template('user_modify.html', form=form) elif request.method == "POST": - if (form.validate_on_submit() and - User(**User().clear_form_data(form.data)).data_validataion()): + if form.validate_on_submit(): if form.id.data: user = db.session.query(User).get(form.id.data) - user.name = form.name.data - user.alias = form.alias.data - user.email = form.email.data - user.role_id = form.role_id.data - user.delete_date = form.delete_date.data + + form.populate_obj(user) db.session.commit() flash("user modified") else: - newuser = User(**User().clear_form_data(form.data)) + newuser = User(**form_data_clean(form.data)) db.session.add(newuser) db.session.commit() flash("user added") diff --git a/requirements.txt b/requirements.txt index 22091a3..9127b24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,6 @@ itsdangerous==0.24 Jinja2==2.9.6 Mako==1.0.7 MarkupSafe==1.0 -pep8==1.7.1 psycopg2==2.7.3.2 python-dateutil==2.6.1 python-editor==1.0.3 From 595f9e4e319793908eba5b4e5324f3ab40ba7f33 Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 3 Nov 2017 00:14:05 +0200 Subject: [PATCH 007/385] rebuild templates, upgrade views, using cli --- README.md | 37 ++++----- backend/app.py | 27 ++++++ backend/app_builder.py | 10 --- backend/config.py.example | 9 +- backend/create_database.py | 83 ++++-------------- backend/drop_database.py | 11 ++- backend/forms/forms.py | 66 ++++++++------- backend/insert_db_data.py | 69 +++++++++++++++ backend/manage.py | 11 --- backend/models/issues.py | 14 ++-- backend/models/users.py | 17 +--- backend/templates/admin_page.html | 39 ++------- backend/templates/base.html | 3 - backend/templates/flash-message.html | 17 ---- backend/templates/index.html | 40 ++------- backend/templates/layouts.html | 52 ++++++++++++ backend/templates/login_page.html | 36 ++------ backend/templates/user_modify.html | 39 ++------- backend/templates/user_page.html | 92 ++++++++------------ backend/views/views.py | 120 ++++++++++++++------------- 20 files changed, 367 insertions(+), 425 deletions(-) create mode 100644 backend/app.py delete mode 100644 backend/app_builder.py create mode 100644 backend/insert_db_data.py delete mode 100644 backend/manage.py delete mode 100644 backend/templates/base.html delete mode 100644 backend/templates/flash-message.html create mode 100644 backend/templates/layouts.html diff --git a/README.md b/README.md index 19d71fa..d5e3925 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,31 @@ # rv-027py -### Installation - - For installing required libs and frameworks execute next commands: +### Installing required libs and frameworks ``` pip install -r requirements.txt ``` ### Configuration - -Copy config.py.example and rename this file to config.py -``` -Set db credentials -``` - -### Migrations and DB - -1) Initialize migration (choose directory with migrations.py) & enter following command -``` -migrations.py db init -``` -2) Create migrations files ``` -migrations.py db migrate +Rename file config.py.example into config.py and +fill config.py with your's database credentials. ``` -3) Create tables from migrations +### Prepare Command Line Interface ``` -migrations.py db upgrade +set FLASK_APP=app.py +set FLASK_DEBUG=1 ``` -Create test records +### Create database and test records. ``` -python create_database.py +flask initdb +flask insertdata ``` ### Run application +``` +flask run +``` -For the start application execute +To drop database (must do: Prepare Command Line Interface step) ``` -python run.py +flask dropdb ``` diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..233c629 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,27 @@ +import click +from flask import Flask +from flask_bootstrap import Bootstrap +from flask_sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config.from_object('config.DevelopmentConfig') +db = SQLAlchemy(app) +Bootstrap(app) + + +from create_database import db_create +from drop_database import db_drop +from insert_db_data import db_insert_data +from views import views + +@app.cli.command() +def initdb(): + db_create() + +@app.cli.command() +def dropdb(): + db_drop() + +@app.cli.command() +def insertdata(): + db_insert_data() diff --git a/backend/app_builder.py b/backend/app_builder.py deleted file mode 100644 index 220c798..0000000 --- a/backend/app_builder.py +++ /dev/null @@ -1,10 +0,0 @@ -import os - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - -app = Flask(__name__) -app.config.from_object('config.DevelopmentConfig') -db = SQLAlchemy(app) - - diff --git a/backend/config.py.example b/backend/config.py.example index 3e41d08..64dad40 100644 --- a/backend/config.py.example +++ b/backend/config.py.example @@ -1,8 +1,6 @@ -# Rename this file to config.py and fill db_credentials with your's data. +# Rename this file to config.py and fill SQLALCHEMY_DATABASE_URI with your's data. class Config(object): - # Change database credentials to yours. - db_credentials = 'postgres://user:password@localhost/db_name' DEBUG = False @@ -11,8 +9,9 @@ class Config(object): WTF_CSRF_ENABLED = True WTF_CSRF_SECRET_KEY = 'SECRET_OR_NOT_KEY' - - SQLALCHEMY_DATABASE_URI = db_credentials + + # Change database credentials to yours. + SQLALCHEMY_DATABASE_URI = 'postgres://user:password@localhost/db_name' SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/backend/create_database.py b/backend/create_database.py index e80b1e0..a146293 100644 --- a/backend/create_database.py +++ b/backend/create_database.py @@ -1,82 +1,29 @@ import os + from sqlalchemy_utils.functions.database import (create_database, database_exists) + +from app import db +from config import Config from models.users import Role, User from models.issues import Attachment, Category, Issue, IssueHistory, Status -from app_builder import db -from config import Config # Checking if database exists, and if not -> create it with all tables. -db_credentials = Config.db_credentials +db_credentials = Config.SQLALCHEMY_DATABASE_URI if 'DATABASE_URL' in os.environ: db_credentials = os.environ['DATABASE_URL'] -if not database_exists(db_credentials): - create_database(db_credentials) - db.create_all() - db.session.commit() - - -# Creating some test data. -role = Role(role='admin') -role1 = Role(role='moderator') -role2 = Role(role='user') - -category = Category(category='road accident', favicon='') -category1 = Category(category='infrastructure accident', favicon='') -category2 = Category(category='another accident', favicon='') - -status1 = Status(status="new") -status2 = Status(status="working") -status3 = Status(status="closed") - -user1 = User(name='Bob', alias='Bobby', email='bob@gmail.com', - password='crypto', role_id='1', avatar=None, delete_date=None) -user2 = User(name='Mark', alias='Marky', email='mark@gmail.com', - password='123', role_id='2', avatar=None, delete_date=None) -user3 = User(name='Maria', alias='Mary', email='maria@gmail.com', - password='321', role_id='3', avatar=None, delete_date=None) - -issue1 = Issue(name='Road accident', user_id='2', category_id='1', location='', - description='Car crash ...', open_date='2017/10/25', - close_date=None, delete_date=None) -issue2 = Issue(name='Road accident', user_id='3', category_id='1', location='', - description='Bus crash ...', open_date='2016/01/12', - close_date='2016/01/20', delete_date=None) -issue3 = Issue(name='Dog lost', user_id='3', category_id='3', location='', - description='Poor puppy is lost', open_date='2017/09/20', - close_date='2017/09/25', delete_date='2017/09/26') - -attachment1 = Attachment(issue_id='1', image_url='some url1', delete_date=None) -attachment2 = Attachment( - issue_id='2', image_url='some url2', delete_date='2016/01/20') -attachment3 = Attachment( - issue_id='3', image_url='some url3', delete_date='2017/09/26') - -issueHistory1 = IssueHistory(user_id='1', issue_id='1', status_id='1', - transaction_date='2017/10/25', delete_date=None) -issueHistory2 = IssueHistory(user_id='1', issue_id='1', status_id='2', - transaction_date='2017/10/27', delete_date=None) -issueHistory3 = IssueHistory(user_id='3', issue_id='3', status_id='1', - transaction_date='2017/09/20', delete_date=None) -issueHistory4 = IssueHistory(user_id='3', issue_id='3', status_id='2', - transaction_date='2017/09/25', delete_date=None) -issueHistory5 = IssueHistory(user_id='3', issue_id='3', status_id='3', - transaction_date='2017/09/26', delete_date=None) - - -# Insert test data into database. -db.session.add_all([role, role1, role2, - category, category1, category2, - status1, status2, status3, - user1, user2, user3, - issue1, issue2, issue3, - issueHistory1, issueHistory2, issueHistory3, - issueHistory4, issueHistory5 - ]) -db.session.commit() +def db_create(): + if not database_exists(db_credentials): + create_database(db_credentials) + db.create_all() + db.session.commit() + print "Successfully created database and tables." + else: + print "Database already exists!" -print "Ok" +if __name__ == '__main__': + db_create() diff --git a/backend/drop_database.py b/backend/drop_database.py index b0d5c2f..621c826 100644 --- a/backend/drop_database.py +++ b/backend/drop_database.py @@ -4,12 +4,15 @@ from config import Config -db_credentials = Config.db_credentials + +db_credentials = Config.SQLALCHEMY_DATABASE_URI if 'DATABASE_URL' in os.environ: db_credentials = os.environ['DATABASE_URL'] -# Dropping our test base. -drop_database(db_credentials) +def db_drop(): + drop_database(db_credentials) + print 'DB dropped' -print 'DB dropped' +if __name__ == '__main__': + db_drop() diff --git a/backend/forms/forms.py b/backend/forms/forms.py index 21f6545..d529475 100644 --- a/backend/forms/forms.py +++ b/backend/forms/forms.py @@ -6,43 +6,51 @@ class UserForm(FlaskForm): + """User info modifying form.""" + id = HiddenField('id') - name = StringField('name', - description=u'Length between 3 and 32 chars', - validators=[ - DataRequired(), - Length(min=3, max=32), - Regexp( - r"^[\w]{3,32}$", - message='Name must be between 3 and 32 chars ' - 'and only letters, numbers and "_" may be used.') - ]) - alias = StringField('alias', - description=u'Length between 3 and 32 chars', - validators=[ - DataRequired(), - Length(min=3, max=32), - Regexp( - r"^[\w]{3,32}$", - message='Alias must be between 3 and 32 chars ' - 'and only letters, numbers and "_" may be used.') - ]) + name = StringField( + 'name', + description=u'Length between 3 and 32 chars.', + validators=[ + DataRequired(), + Length(min=3, max=32), + Regexp( + r"^[\w]+$", + message='Only letters, numbers and "_" may be used.') + ] + ) + alias = StringField( + 'alias', + description=u'Length between 3 and 32 chars.', + validators=[ + DataRequired(), + Length(min=3, max=32), + Regexp( + r"^[\w]+$", + message='Only letters, numbers and "_" may be used.') + ] + ) email = StringField('email', validators=[Email()]) - role_id = SelectField('role_id', choices=[ - ('1', 'admin'), - ('2', 'moderator'), - ('3', 'user')], - validators=[DataRequired()]) - delete_date = DateField('delete_date', - default=None, - description=u'Enter date in YYYY-MM-DD value', - validators=[Optional()], format='%Y-%m-%d') + + role_id = SelectField( + 'role_id', + choices=[ + ('1', 'admin'), + ('2', 'moderator'), + ('3', 'user') + ], + validators=[DataRequired()] + ) + submit_button = SubmitField('Save') class LoginForm(FlaskForm): + """Login form.""" + email = StringField('login', validators=[Email()]) password = PasswordField('password', validators=[DataRequired()]) submit_button = SubmitField('Login') diff --git a/backend/insert_db_data.py b/backend/insert_db_data.py new file mode 100644 index 0000000..cc20c2b --- /dev/null +++ b/backend/insert_db_data.py @@ -0,0 +1,69 @@ +import os + +from app import db +from config import Config +from models.users import Role, User +from models.issues import Attachment, Category, Issue, IssueHistory, Status + +db_credentials = Config.SQLALCHEMY_DATABASE_URI + +if 'DATABASE_URL' in os.environ: + db_credentials = os.environ['DATABASE_URL'] + +role = Role(role='admin') +role1 = Role(role='moderator') +role2 = Role(role='user') + +category = Category(category='road accident', favicon='') +category1 = Category(category='infrastructure accident', favicon='') +category2 = Category(category='another accident', favicon='') + +status1 = Status(status="new") +status2 = Status(status="working") +status3 = Status(status="closed") + +user1 = User(name='Bob', alias='Bobby', email='bob@gmail.com', + password='crypto', role_id='1') +user2 = User(name='Mark', alias='Marky', email='mark@gmail.com', + password='123', role_id='2') +user3 = User(name='Maria', alias='Mary', email='maria@gmail.com', + password='321', role_id='3') + +issue1 = Issue(name='Road accident', user_id='2', category_id='1', location='', + description='Car crash ...') +issue2 = Issue(name='Road accident', user_id='3', category_id='1', location='', + description='Bus crash ...') +issue3 = Issue(name='Dog lost', user_id='3', category_id='3', location='', + description='Poor puppy is lost') + +attachment1 = Attachment(issue_id='1', image_url='some url1') +attachment2 = Attachment(issue_id='2', image_url='some url2') +attachment3 = Attachment(issue_id='3', image_url='some url3') + +issueHistory1 = IssueHistory(user_id='1', issue_id='1', status_id='1', + transaction_date='2017/10/25') +issueHistory2 = IssueHistory(user_id='1', issue_id='1', status_id='2', + transaction_date='2017/10/27') +issueHistory3 = IssueHistory(user_id='3', issue_id='3', status_id='1', + transaction_date='2017/09/20') +issueHistory4 = IssueHistory(user_id='3', issue_id='3', status_id='2', + transaction_date='2017/09/25') +issueHistory5 = IssueHistory(user_id='3', issue_id='3', status_id='3', + transaction_date='2017/09/26') + + +def db_insert_data(): + db.session.add_all([role, role1, role2, + category, category1, category2, + status1, status2, status3, + user1, user2, user3, + issue1, issue2, issue3, + issueHistory1, issueHistory2, issueHistory3, + issueHistory4, issueHistory5 + ]) + db.session.commit() + + print "Test data have been inserted into database." + +if __name__ == '__main__': + db_insert_data() \ No newline at end of file diff --git a/backend/manage.py b/backend/manage.py deleted file mode 100644 index 77e223c..0000000 --- a/backend/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - -from flask_bootstrap import Bootstrap -from app_builder import app, db - -from views import views - -Bootstrap(app) - -if __name__ == '__main__': - app.run() diff --git a/backend/models/issues.py b/backend/models/issues.py index 6eba75e..c931132 100644 --- a/backend/models/issues.py +++ b/backend/models/issues.py @@ -1,4 +1,4 @@ -from app_builder import db +from app import db class Attachment(db.Model): @@ -9,7 +9,7 @@ class Attachment(db.Model): id = db.Column(db.Integer, primary_key=True) issue_id = db.Column(db.ForeignKey(u'issues.id'), index=True) image_url = db.Column(db.Text) - delete_date = db.Column(db.Date) + delete_date = db.Column(db.TIMESTAMP) issue = db.relationship(u'Issue') @@ -33,8 +33,8 @@ class IssueHistory(db.Model): user_id = db.Column(db.ForeignKey(u'users.id')) issue_id = db.Column(db.ForeignKey(u'issues.id'), index=True) status_id = db.Column(db.ForeignKey(u'statuses.id'), index=True) - transaction_date = db.Column(db.Date) - delete_date = db.Column(db.Date) + transaction_date = db.Column(db.TIMESTAMP) + delete_date = db.Column(db.TIMESTAMP) issue = db.relationship(u'Issue') status = db.relationship(u'Status') @@ -53,9 +53,9 @@ class Issue(db.Model): u'category.id'), nullable=False, index=True) location = db.Column(db.Text) description = db.Column(db.Text) - open_date = db.Column(db.Date) - close_date = db.Column(db.Date) - delete_date = db.Column(db.Date) + open_date = db.Column(db.TIMESTAMP) + close_date = db.Column(db.TIMESTAMP) + delete_date = db.Column(db.TIMESTAMP) category = db.relationship(u'Category') user = db.relationship(u'User') diff --git a/backend/models/users.py b/backend/models/users.py index 06d3559..2837dbc 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -1,8 +1,8 @@ -# import re -from app_builder import db +from app import db class Role(db.Model): + """This class is used for role table in database.""" __tablename__ = 'roles' @@ -12,6 +12,7 @@ class Role(db.Model): class User(db.Model): + """This class is used for user table in database.""" __tablename__ = 'users' @@ -23,17 +24,7 @@ class User(db.Model): password = db.Column(db.Text) role_id = db.Column(db.ForeignKey(u'roles.id'), index=True) avatar = db.Column(db.Text) - delete_date = db.Column(db.Date) + delete_date = db.Column(db.TIMESTAMP) role = db.relationship(u'Role') - # def data_validataion(self): - # """Validate the user's data""" - # self.name_check = re.match(r'^[\w]{4,32}$', self.name) - # self.alias_check = re.match(r'^[\w]{3,32}$', self.alias) - # self.email_check = re.match(r'^[^@]+@[^@]+.[^@]+$', self.email) - # self.role_id_check = re.match(r'^[1-3]$', self.role_id) - # if (self.name_check and self.alias_check and - # self.email_check and self.role_id_check): - # return True - # return False diff --git a/backend/templates/admin_page.html b/backend/templates/admin_page.html index ff31438..7fd7e01 100644 --- a/backend/templates/admin_page.html +++ b/backend/templates/admin_page.html @@ -1,35 +1,14 @@ -{% extends "base.html" %} +{% extends 'layouts.html' %} -{% block navbar %} -
-
-
- - - -
-
-
-{% endblock %} +{% block title_suffix %}Admins managements{% endblock title_suffix %} +{% block navbar_li %} +
  • Main page
  • +
  • Admin page
  • +
  • Logout
  • +{% endblock navbar_li %} {% block content %} - - {% include 'flash-message.html' %} - -
    -
    -
    -

    Welcome to the Users management

    - Work with users -
    -
    -
    - +

    Welcome to the Admins managements

    + Work with users {% endblock %} \ No newline at end of file diff --git a/backend/templates/base.html b/backend/templates/base.html deleted file mode 100644 index 329dae1..0000000 --- a/backend/templates/base.html +++ /dev/null @@ -1,3 +0,0 @@ -{% extends "bootstrap/base.html" %} -{% import "bootstrap/wtf.html" as wtf %} - diff --git a/backend/templates/flash-message.html b/backend/templates/flash-message.html deleted file mode 100644 index d946e95..0000000 --- a/backend/templates/flash-message.html +++ /dev/null @@ -1,17 +0,0 @@ -{% block flash_message %} -
    -
    -
    - {% with messages = get_flashed_messages() %} - {% if messages %} -
      - {% for message in messages %} -
    • {{ message }}
    • - {% endfor %} -
    - {% endif %} - {% endwith %} -
    -
    -
    -{% endblock %} \ No newline at end of file diff --git a/backend/templates/index.html b/backend/templates/index.html index a1a0237..da14755 100644 --- a/backend/templates/index.html +++ b/backend/templates/index.html @@ -1,37 +1,15 @@ -{% extends 'bootstrap/base.html' %} +{% extends 'layouts.html' %} -{% block title %}CityView{% endblock %} -{% block navbar %} -
    -
    -
    - - - -
    -
    -
    -{% endblock %} +{% block title_suffix %}Main page{% endblock title_suffix %} +{% block navbar_li %} +
  • Main page
  • +
  • Admin page
  • +
  • Login
  • +
  • Logout
  • +{% endblock navbar_li %} {% block content %} - - {% include 'flash-message.html' %} - -
    -
    -
    -

    Welcome to the CityView

    -
    -
    -
    - +

    Welcome to the CityView

    {% endblock %} diff --git a/backend/templates/layouts.html b/backend/templates/layouts.html new file mode 100644 index 0000000..fbad8be --- /dev/null +++ b/backend/templates/layouts.html @@ -0,0 +1,52 @@ +{% extends "bootstrap/base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + + +{% block title %} + City View - {% block title_suffix %}{% endblock title_suffix %} +{% endblock title %} + +{% block body %} + + {% block navbar %} +
    +
    +
    + +
    +
    +
    + {% endblock navbar %} + +
    +
    +
    + {% with messages = get_flashed_messages() %} + {% if messages %} +
      + {% for message in messages %} +
    • {{ message }}
    • + {% endfor %} +
    + {% endif %} + {% endwith %} +
    +
    +
    + +
    +
    +
    + {% block content %} + {% endblock content %} +
    +
    +
    + +{% endblock body %} + diff --git a/backend/templates/login_page.html b/backend/templates/login_page.html index ba64ed2..161024c 100644 --- a/backend/templates/login_page.html +++ b/backend/templates/login_page.html @@ -1,37 +1,17 @@ -{% extends "base.html" %} +{% extends 'layouts.html' %} -{% block title %}CityView{% endblock %} +{% block title_suffix %}Login page{% endblock title_suffix %} -{% block navbar %} -
    -
    -
    - - - -
    -
    -
    -{% endblock %} +{% block navbar_li %} +
  • Main page
  • +{% endblock navbar_li %} {% block content %} - - {% include 'flash-message.html' %} - -
    -
    -
    -

    Login page

    - {{ wtf.quick_form(form, action="/login", method="POST", button_map={'submit_button' : 'success'}) }} -
    -
    +
    +

    Login page

    + {{ wtf.quick_form(form, action=url_for('login'), method="POST", button_map={'submit_button' : 'success'}) }}
    - {% endblock %} \ No newline at end of file diff --git a/backend/templates/user_modify.html b/backend/templates/user_modify.html index f45b447..07f08fc 100644 --- a/backend/templates/user_modify.html +++ b/backend/templates/user_modify.html @@ -1,37 +1,16 @@ -{% extends '/base.html' %} +{% extends 'layouts.html' %} -{% block title %}CityView - Users view{% endblock %} - -{% block navbar %} -
    -
    -
    - - - -
    -
    -
    -{% endblock %} +{% block title_suffix %}Users add/edit{% endblock title_suffix %} +{% block navbar_li %} +
  • Main page
  • +
  • Logout
  • +{% endblock navbar_li %} {% block content %} - - {% include 'flash-message.html' %} - -
    -
    -
    -

    User modifying form

    - {{ wtf.quick_form(form, action="/user_modify", method="POST", button_map={'submit_button' : 'success'}) }} -
    -
    +

    User modifying form

    +
    + {{ wtf.quick_form(form, action=route_to, method="POST", button_map={'submit_button' : 'success'}) }}
    - {% endblock %} \ No newline at end of file diff --git a/backend/templates/user_page.html b/backend/templates/user_page.html index 200cb41..ea92e50 100644 --- a/backend/templates/user_page.html +++ b/backend/templates/user_page.html @@ -1,65 +1,41 @@ -{% extends 'base.html' %} +{% extends 'layouts.html' %} -{% block title %}CityView - Admin page {% endblock %} - - -{% block navbar %} -
    -
    -
    - - - -
    -
    -
    -{% endblock %} +{% block title_suffix %}Users managements{% endblock title_suffix %} +{% block navbar_li %} +
  • Main page
  • +
  • Logout
  • +{% endblock navbar_li %} {% block content %} - - {% include 'flash-message.html' %} - -
    -
    -
    -
    - - - - - - - - - - - - - {% for user in users %} - - - - - - - - - - - - {% endfor %} -
    IdUserAliasE-mailRoleAvatarDelete date
    {{ user[0].id }}{{ user[0].name }}{{ user[0].alias }}{{ user[0].email }}{{ user[1].role }}{{ user[0].avatar }}{{ user[0].delete_date }}EditDelete
    -
    - Add new user -
    -
    +
    + + + + + + + + + + + + + {% for user, role in users %} + + + + + + + + + + + + {% endfor %} +
    IdUserAliasE-mailRoleAvatarDelete date
    {{ user.id }}{{ user.name }}{{ user.alias }}{{ user.email }}{{ role.role }}{{ user.avatar }}{{ user.delete_date }}EditDelete
    - + Add new user {% endblock %} \ No newline at end of file diff --git a/backend/views/views.py b/backend/views/views.py index e704f11..0f70131 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -1,32 +1,27 @@ from datetime import datetime from functools import wraps -from flask import flash, render_template, redirect, request, session, url_for +from flask import flash, redirect, request, render_template, session, url_for -from app_builder import app, db +from app import app, db from forms.forms import LoginForm, UserForm from models.users import Role, User +ROLE_ADMIN = 1 +ROLE_MODERATOR = 2 +ROLE_USER = 3 def admin_permissions(func): """Decorator to check admin rights to access some route.""" @wraps(func) def wrapper(*args, **kwargs): - if not 'user_id' in session or session['role_id'] != 1: + if not 'user_id' in session or session['role_id'] != ROLE_ADMIN: flash("Don't have access") - return redirect(url_for('index')) + return redirect(url_for('login')) return func(*args, **kwargs) return wrapper -def form_data_clean(data): - """Remove unnessery fields from form.data.""" - data.pop('csrf_token', None) - data.pop('submit_button', None) - data.pop('id', None) - return data - - @app.route('/') @app.route('/index') def index(): @@ -39,7 +34,7 @@ def admin(): return render_template('admin_page.html') -@app.route('/user_page') +@app.route('/userpage') @admin_permissions def user_page(): users = db.session.query(User, Role).filter( @@ -47,48 +42,57 @@ def user_page(): return render_template('user_page.html', users=users) -@app.route("/user_modify", methods=['GET', 'POST']) +@app.route('/useradd', methods=['GET', 'POST']) @admin_permissions -def user_modify(): +def user_add(): + route_to = url_for('user_add') form = UserForm(request.form) - if 'id' in request.args: - chosen_one = db.session.query(User).get(request.args.get('id')) - form = UserForm(obj=chosen_one) - if request.method == "GET": - return render_template('user_modify.html', form=form) - - elif request.method == "POST": - - if form.validate_on_submit(): - if form.id.data: - user = db.session.query(User).get(form.id.data) + return render_template('user_modify.html', form=form, route_to=route_to) + + if form.validate_on_submit(): + newuser = User() + newuser.name = form.name.data + newuser.alias = form.alias.data + newuser.role_id = form.role_id.data + newuser.email = form.email.data + db.session.add(newuser) + db.session.commit() + flash("user added") + return redirect(url_for('user_page')) + else: + flash("wrong data") + return render_template('user_modify.html', form=form, route_to=route_to) - form.populate_obj(user) - db.session.commit() - flash("user modified") - else: - newuser = User(**form_data_clean(form.data)) - db.session.add(newuser) - db.session.commit() - flash("user added") - return redirect(url_for('user_page')) - else: - flash("wrong data") - return render_template('user_modify.html', form=form) +@app.route('/usermodify/', methods=['GET', 'POST']) +@admin_permissions +def user_modify(users_id): + route_to = url_for('user_modify', users_id=users_id) + user = db.session.query(User).get(users_id) + form = UserForm(obj=user) + if request.method == "GET": + return render_template('user_modify.html', form=form, route_to=route_to) -@app.route('/delete_user') -def delete_user(): - if 'id' in request.args: - today = datetime.today().strftime('%Y-%m-%d') - user = db.session.query(User).get(request.args.get('id')) - user.delete_date = today + if form.validate_on_submit(): + form.populate_obj(user) db.session.commit() - flash("user deleted") - + flash("user modified") + return redirect(url_for('user_page')) + else: + flash("wrong data") + return render_template('user_modify.html', form=form, route_to=route_to) + + +@app.route('/deleteuser/') +def delete_user(users_id): + current_moment = datetime.now() + user = db.session.query(User).get(users_id) + user.delete_date = current_moment + db.session.commit() + flash("user deleted") return redirect(url_for('user_page')) @@ -97,21 +101,21 @@ def login(): form = LoginForm(request.form) if request.method == 'GET': return render_template('login_page.html', form=form) - if request.method == 'POST': - if form.validate_on_submit(): - user = db.session.query(User).filter( - User.email == form.email.data).first() - if user and user.password == form.password.data: - session['user_id'] = user.id - session['role_id'] = user.role_id - flash('Wellcome %s' % user.name) - return redirect(url_for('index')) - else: - flash('Incorrect login/password data...') - return render_template('login_page.html', form=form) + + if form.validate_on_submit(): + user = db.session.query(User).filter( + User.email == form.email.data).first() + if user and user.password == form.password.data: + session['user_id'] = user.id + session['role_id'] = user.role_id + flash('Wellcome %s' % user.name) + return redirect(url_for('index')) else: flash('Incorrect login/password data...') return render_template('login_page.html', form=form) + else: + flash('Incorrect login/password data...') + return render_template('login_page.html', form=form) @app.route('/logout') From 0821c872cc39a76bb67715231746caca076defe0 Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 3 Nov 2017 00:19:04 +0200 Subject: [PATCH 008/385] PEP8 fix --- backend/app.py | 9 ++++++--- backend/insert_db_data.py | 2 +- backend/models/users.py | 3 +-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/app.py b/backend/app.py index 233c629..a41a176 100644 --- a/backend/app.py +++ b/backend/app.py @@ -14,14 +14,17 @@ from insert_db_data import db_insert_data from views import views + @app.cli.command() def initdb(): - db_create() + db_create() + @app.cli.command() def dropdb(): - db_drop() + db_drop() + @app.cli.command() def insertdata(): - db_insert_data() + db_insert_data() diff --git a/backend/insert_db_data.py b/backend/insert_db_data.py index cc20c2b..bc55059 100644 --- a/backend/insert_db_data.py +++ b/backend/insert_db_data.py @@ -66,4 +66,4 @@ def db_insert_data(): print "Test data have been inserted into database." if __name__ == '__main__': - db_insert_data() \ No newline at end of file + db_insert_data() diff --git a/backend/models/users.py b/backend/models/users.py index 2837dbc..e2e9ae2 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -12,7 +12,7 @@ class Role(db.Model): class User(db.Model): - + """This class is used for user table in database.""" __tablename__ = 'users' @@ -27,4 +27,3 @@ class User(db.Model): delete_date = db.Column(db.TIMESTAMP) role = db.relationship(u'Role') - From aa5e9b16c7a55c31ad71225365841ace5618b052 Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 3 Nov 2017 07:46:21 +0200 Subject: [PATCH 009/385] fix README and requirements --- README.md | 7 ++++--- requirements.txt | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d5e3925..8c02ebc 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,12 @@ Rename file config.py.example into config.py and fill config.py with your's database credentials. ``` ### Prepare Command Line Interface +On Windows use **set** instead of **export** ``` -set FLASK_APP=app.py -set FLASK_DEBUG=1 +export FLASK_APP=app.py +export FLASK_DEBUG=1 ``` -### Create database and test records. +### Create database and test records ``` flask initdb flask insertdata diff --git a/requirements.txt b/requirements.txt index 9127b24..1f16af6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ dominate==2.3.1 Flask==0.12.2 Flask-Bootstrap==3.3.7.1 Flask-Migrate==2.1.1 -Flask-Script==2.0.6 Flask-SQLAlchemy==2.3.2 Flask-WTF==0.14.2 inflect==0.2.5 From 3d6121ccbfafcad5cb8409a9315c27e0916eb8bf Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Fri, 3 Nov 2017 22:37:40 +0200 Subject: [PATCH 010/385] search users --- backend/config.py.example | 24 -------------------- backend/forms/forms.py | 25 ++++++++++++++++++++ backend/templates/user_page.html | 4 ++++ backend/views/views.py | 39 +++++++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 27 deletions(-) delete mode 100644 backend/config.py.example diff --git a/backend/config.py.example b/backend/config.py.example deleted file mode 100644 index 64dad40..0000000 --- a/backend/config.py.example +++ /dev/null @@ -1,24 +0,0 @@ -# Rename this file to config.py and fill SQLALCHEMY_DATABASE_URI with your's data. - -class Config(object): - - DEBUG = False - - SECRET_KEY = 'SECRET_OR_NOT_KEY' - - WTF_CSRF_ENABLED = True - - WTF_CSRF_SECRET_KEY = 'SECRET_OR_NOT_KEY' - - # Change database credentials to yours. - SQLALCHEMY_DATABASE_URI = 'postgres://user:password@localhost/db_name' - - SQLALCHEMY_TRACK_MODIFICATIONS = False - - -class ProductionConfig(Config): - DEBUG = False - - -class DevelopmentConfig(Config): - DEBUG = True diff --git a/backend/forms/forms.py b/backend/forms/forms.py index d529475..cc46804 100644 --- a/backend/forms/forms.py +++ b/backend/forms/forms.py @@ -54,3 +54,28 @@ class LoginForm(FlaskForm): email = StringField('login', validators=[Email()]) password = PasswordField('password', validators=[DataRequired()]) submit_button = SubmitField('Login') + +class SearchForm(FlaskForm): + + """Search form""" + + search = StringField( + 'search', + validators=[ + DataRequired(), + Length(min=2) + ] + ) + field_by = SelectField( + 'by', + choices=[ + (1, 'name'), + (2, 'alias'), + (3, 'email'), + (4, 'name+alias'), + (5, 'alias+email'), + (6, 'email+name'), + (7, 'email+name+alias') + ] + ) + diff --git a/backend/templates/user_page.html b/backend/templates/user_page.html index ea92e50..aa1efca 100644 --- a/backend/templates/user_page.html +++ b/backend/templates/user_page.html @@ -9,6 +9,10 @@ {% endblock navbar_li %} {% block content %} +
    +
    Search user: {{form.search(size=40)}} by: {{form.field_by() }} +
    +
    diff --git a/backend/views/views.py b/backend/views/views.py index 0f70131..615dc0a 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -2,14 +2,16 @@ from functools import wraps from flask import flash, redirect, request, render_template, session, url_for +from sqlalchemy import and_, or_ from app import app, db -from forms.forms import LoginForm, UserForm +from forms.forms import LoginForm, SearchForm, UserForm from models.users import Role, User ROLE_ADMIN = 1 ROLE_MODERATOR = 2 ROLE_USER = 3 +MIN_SEARCH_STR = 2 def admin_permissions(func): """Decorator to check admin rights to access some route.""" @@ -34,12 +36,43 @@ def admin(): return render_template('admin_page.html') -@app.route('/userpage') +@app.route('/userpage', methods=['GET']) @admin_permissions def user_page(): + form = SearchForm(request.form) users = db.session.query(User, Role).filter( User.role_id == Role.id).order_by(User.id).all() - return render_template('user_page.html', users=users) + if request.args.get('search') != "" and request.args.get('search') >= MIN_SEARCH_STR: + # if form.validate_on_submit(): + key = int(request.args.get('field_by')) + search_string = str(request.args.get('search')) + search_paremeters = [] + search_users = [] + for one_string in search_string.split(' '): + if len(one_string) >= MIN_SEARCH_STR: + search_paremeters.append(''.join(["%", one_string, "%"])) + for search_paremeter in search_paremeters: + conditions = {1:"users.name LIKE '%s'" %(search_paremeter), + 2:"users.alias LIKE '%s'" %(search_paremeter), + 3:"users.email LIKE '%s'" %(search_paremeter)} + conditions[4] = ' '.join([conditions[1], "OR", conditions[2]]) + conditions[5] = ' '.join([conditions[2], "OR", conditions[3]]) + conditions[6] = ' '.join([conditions[1], "OR", conditions[3]]) + conditions[7] = ' '.join([conditions[1], "OR", conditions[2], "OR", conditions[3]]) + condition = conditions.get(key) + results = db.session.query(User, Role).filter(and_( + User.role_id == Role.id, str(condition))).distinct(User.id).order_by(User.id).all() + for user in results: + if not user in search_users: + search_users.append(user) + search_users.sort(key=lambda user: user[0].id) + if search_users == []: + flash("Search didn`t give result") + return redirect(url_for('user_page')) + flash("Search results") + return render_template('user_page.html', form=form, users=search_users) + else: + return render_template('user_page.html', form=form, users=users) @app.route('/useradd', methods=['GET', 'POST']) From 33f45a788a076144d4d37f9b705dfdb582ab1d10 Mon Sep 17 00:00:00 2001 From: alevitsky Date: Sun, 5 Nov 2017 00:08:27 +0200 Subject: [PATCH 011/385] updated delete, restore, last admin, confirmation --- backend/static/app.js | 8 ++++++++ backend/templates/layouts.html | 5 ++++- backend/templates/user_page.html | 26 +++++++++++++++++++++++++- backend/views/views.py | 30 ++++++++++++++++++++++++++++-- 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 backend/static/app.js diff --git a/backend/static/app.js b/backend/static/app.js new file mode 100644 index 0000000..4847cb9 --- /dev/null +++ b/backend/static/app.js @@ -0,0 +1,8 @@ +$( document ).ready(function() { + $('#deletion').on('show.bs.modal', function(e) { + var userId = $(e.relatedTarget).data('user-id'); + var userName = $(e.relatedTarget).data('user-name'); + $(".modal-title").text("Confirm deletion of " + userName); + $("#delete-user").attr('href', '/deleteuser/' + userId); + }); +}); \ No newline at end of file diff --git a/backend/templates/layouts.html b/backend/templates/layouts.html index fbad8be..6daca90 100644 --- a/backend/templates/layouts.html +++ b/backend/templates/layouts.html @@ -47,6 +47,9 @@ - +{% block scripts %} + {{ super() }} + +{%- endblock %} {% endblock body %} diff --git a/backend/templates/user_page.html b/backend/templates/user_page.html index ea92e50..943967e 100644 --- a/backend/templates/user_page.html +++ b/backend/templates/user_page.html @@ -32,10 +32,34 @@ - + {% if not user.delete_date %} + + {% else %} + + {% endif %} {% endfor %}
    {{ user.avatar }} {{ user.delete_date }} EditDeleteDeleteRestore
    Add new user + + + Add new user +
    +{% endblock content %} + + +{% block extra_js %} + + + {% load static %} + + +{% endblock extra_js %} diff --git a/client/apps/city_issues/templates/issues_list.html b/client/apps/city_issues/templates/issues_list.html index 6a00b2d..ae1977d 100644 --- a/client/apps/city_issues/templates/issues_list.html +++ b/client/apps/city_issues/templates/issues_list.html @@ -60,7 +60,7 @@

    All Issues:

    {% for issue in issues_list %} {{ forloop.counter }} - {{ issue.title }} + {{ issue.title }} {{ issue.status }} {{ issue.description }} {{ issue.category }} diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index ac67861..ac842f6 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -15,7 +15,7 @@ from django.utils.timezone import make_aware from django.views.generic import CreateView, ListView -from city_issues.models import Attachments, Issues, IssueHistory, User +from city_issues.models import Attachments, Issues, IssueHistory, User, Comments from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm @@ -187,3 +187,27 @@ def get_context_data(self, **kwargs): context = super(CheckIssues, self).get_context_data(**kwargs) context['issues_range'] = range(context["paginator"].num_pages) return context + + +class CommentIssues(CreateView): + """Comment issue""" + template_name = 'comment_issue.html' + model = Comments + fields = ['comment'] + + def get_context_data(self, **kwargs): + context = super(CommentIssues, self).get_context_data(**kwargs) + context['issue'] = Issues.objects.get(pk=self.kwargs['pk']) + return context + + def form_valid(self, form): + form = form.save(commit=False) + issue = Issues.objects.get(pk=self.kwargs['pk']) + user = User.objects.get(pk=self.request.user.id) + form.issue = issue + form.user = user + form.save() + return redirect(reverse('issue-comment', kwargs={'pk': self.kwargs['pk']})) + + def form_invalid(self, form): + return super(CommentIssues, self).form_invalid(form) diff --git a/client/basic/urls.py b/client/basic/urls.py index 678df8b..8cb027c 100644 --- a/client/basic/urls.py +++ b/client/basic/urls.py @@ -22,13 +22,13 @@ from city_issues.views import ( CheckIssues, edit_issue_view, get_all_issues_data, get_issue_data, - HomePageView, map_page_view, IssueCreate, UserProfileView) + HomePageView, map_page_view, IssueCreate, UserProfileView, CommentIssues) urlpatterns = [ url(r'^$', HomePageView.as_view(), name='home'), url(r'^issues/$', CheckIssues.as_view(), name='issues'), - + url(r'^issue-comment/(?P[0-9]+)/$', CommentIssues.as_view(), name='issue-comment'), url(r'^map/$', map_page_view, name='map'), url(r'^map/getissuebyid/(?P[0-9]+)$', From 912ce720628319f241f55ccafb61f14ead0f3f4a Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Sun, 3 Dec 2017 16:58:33 +0200 Subject: [PATCH 172/385] search issue --- client/apps/city_issues/forms/forms.py | 11 +++++++++++ .../city_issues/templates/issues_list.html | 14 +++++++++----- client/apps/city_issues/views.py | 19 +++++++++++++++---- client/basic/urls.py | 3 ++- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index ecc8002..bae72e6 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -104,3 +104,14 @@ class IssueFilter(forms.Form): 'placeholder': 'Max length 20 chars', }), required=False,) + + +class IssueSearchForm(forms.Form): + "Issue search form" + search = forms.CharField( + min_length=2, + max_length=100, + widget=forms.TextInput(attrs={ + 'size':'60%', + }), + required=False,) diff --git a/client/apps/city_issues/templates/issues_list.html b/client/apps/city_issues/templates/issues_list.html index 6a00b2d..4877551 100644 --- a/client/apps/city_issues/templates/issues_list.html +++ b/client/apps/city_issues/templates/issues_list.html @@ -11,17 +11,21 @@
    - {% with order_by=request.GET.order_by reverse=request.GET.reverse %} + {% with search=request.GET.search order_by=request.GET.order_by reverse=request.GET.reverse %}
    # - + Title {% if order_by == 'title' and reverse != 'v_v' %}↑ {% elif order_by != 'status' and order_by != 'description' and order_by != 'category' and reverse != 'v_v' %}↑ @@ -30,7 +34,7 @@

    All Issues:

    - + Status {% if order_by == 'status' and reverse != 'v_v' %}↑ {% elif order_by == 'status' and reverse == 'v_v' %}↓ @@ -38,7 +42,7 @@

    All Issues:

    - + Description {% if order_by == 'description' and reverse != 'v_v' %}↑ {% elif order_by == 'description' and reverse == 'v_v' %}↓ @@ -46,7 +50,7 @@

    All Issues:

    - + Category {% if order_by == 'category' and reverse != 'v_v' %}↑ {% elif order_by == 'category' and reverse == 'v_v' %}↓ diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index ac67861..c753daf 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -4,6 +4,7 @@ # -*- coding: utf-8 -*- import json from datetime import date, datetime, time +import operator from django.db.models import Q from django.views.generic.base import TemplateView, View @@ -13,10 +14,10 @@ from django.shortcuts import redirect, render from django.urls import reverse from django.utils.timezone import make_aware -from django.views.generic import CreateView, ListView +from django.views.generic import CreateView, FormView, ListView -from city_issues.models import Attachments, Issues, IssueHistory, User -from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm +from city_issues.models import Attachments, Issues, IssueHistory, User +from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, IssueSearchForm class HomePageView(TemplateView): @@ -166,18 +167,28 @@ def convert_date(obj): return obj -class CheckIssues(ListView): +class CheckIssues(ListView, FormView): """A list of issues""" + form_class = IssueSearchForm template_name = 'issues_list.html' model = Issues context_object_name = 'issues_list' paginate_by = 6 + def get_queryset(self): """Adds sorting""" queryset = super(CheckIssues, self).get_queryset() order_by = self.request.GET.get('order_by', 'title') + search = self.request.GET.get('search') + if search: + query_list = search.split() + queryset = queryset.filter( + reduce(operator.or_, (Q(title__icontains=q) for q in query_list)) | + reduce(operator.or_, (Q(description__icontains=q) for q in query_list)) + ) if order_by in ('title', 'status', 'description', 'category'): + print queryset queryset = queryset.order_by(order_by) if self.request.GET.get('reverse', '') == 'v_v': queryset = queryset.reverse() diff --git a/client/basic/urls.py b/client/basic/urls.py index 678df8b..60eadd0 100644 --- a/client/basic/urls.py +++ b/client/basic/urls.py @@ -23,11 +23,12 @@ from city_issues.views import ( CheckIssues, edit_issue_view, get_all_issues_data, get_issue_data, HomePageView, map_page_view, IssueCreate, UserProfileView) +from city_issues.models import Issues urlpatterns = [ url(r'^$', HomePageView.as_view(), name='home'), - url(r'^issues/$', CheckIssues.as_view(), name='issues'), + url(r'^issues/$', CheckIssues.as_view(queryset=Issues.objects.filter(status='new')), name='issues'), url(r'^map/$', map_page_view, name='map'), From 4172031097df06a6bf6311e5f92c7b0b5b7159bd Mon Sep 17 00:00:00 2001 From: oksana-pidhurska Date: Sun, 3 Dec 2017 12:26:54 -0500 Subject: [PATCH 173/385] fix pep8 --- client/apps/city_issues/templates/comment_issue.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/apps/city_issues/templates/comment_issue.html b/client/apps/city_issues/templates/comment_issue.html index f845f76..c84403c 100644 --- a/client/apps/city_issues/templates/comment_issue.html +++ b/client/apps/city_issues/templates/comment_issue.html @@ -34,10 +34,10 @@

    Comment issue

    - - - - + + + + From 87119c501bdd1f0c95ece4ecb5e112a9c2e5cd78 Mon Sep 17 00:00:00 2001 From: oksana-pidhurska Date: Sun, 3 Dec 2017 12:33:18 -0500 Subject: [PATCH 174/385] add login required mixin --- client/apps/city_issues/mixins.py | 9 +++++++++ client/apps/city_issues/views.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 client/apps/city_issues/mixins.py diff --git a/client/apps/city_issues/mixins.py b/client/apps/city_issues/mixins.py new file mode 100644 index 0000000..bde2335 --- /dev/null +++ b/client/apps/city_issues/mixins.py @@ -0,0 +1,9 @@ +from django.contrib.auth.decorators import login_required +from django.utils.decorators import method_decorator + + +class LoginRequiredMixin(object): + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super(LoginRequiredMixin, self).dispatch(*args, **kwargs) diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index ac842f6..2e3c756 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -17,6 +17,7 @@ from city_issues.models import Attachments, Issues, IssueHistory, User, Comments from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm +from city_issues.mixins import LoginRequiredMixin class HomePageView(TemplateView): @@ -189,7 +190,7 @@ def get_context_data(self, **kwargs): return context -class CommentIssues(CreateView): +class CommentIssues(LoginRequiredMixin, CreateView): """Comment issue""" template_name = 'comment_issue.html' model = Comments From 3210fbad5c7a6d140b8da5c749ff52f3f3667b32 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 3 Dec 2017 22:16:35 +0200 Subject: [PATCH 175/385] upgrade map edit --- client/apps/city_issues/forms/forms.py | 5 +++ .../static/city_issues/js/edit_on_map.js | 41 +++++++++++++++++++ .../city_issues/static/city_issues/js/main.js | 22 ++++++++-- .../city_issues/templates/edit_issue.html | 30 ++++++++++++-- client/apps/city_issues/views.py | 27 +++++------- client/basic/urls.py | 6 +-- 6 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 client/apps/city_issues/static/city_issues/js/edit_on_map.js diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index ecc8002..0b98cd9 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -104,3 +104,8 @@ class IssueFilter(forms.Form): 'placeholder': 'Max length 20 chars', }), required=False,) + + +class IssueFormEdit(IssueForm): + + files = None diff --git a/client/apps/city_issues/static/city_issues/js/edit_on_map.js b/client/apps/city_issues/static/city_issues/js/edit_on_map.js new file mode 100644 index 0000000..da59abd --- /dev/null +++ b/client/apps/city_issues/static/city_issues/js/edit_on_map.js @@ -0,0 +1,41 @@ +(function(){ + function IssueMap() { + this.map = L.map('map'); + this.lat = localStorage.getItem('lat'); + this.lng = localStorage.getItem('lng'); + this.scale = 16; + + if (this.lat && this.lng) { + this.map.setView([this.lat, this.lng], this.scale); + } else { + this.map.locate({setView: true, maxZoom: 50}); + } + + this.layer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(this.map); + + this.marker = L.marker([this.lat, this.lng], { draggable: false}).addTo(this.map); + + this.map.on('click',($.proxy(this.getLocation, this))); + + } + +IssueMap.prototype.getLocation = function (e) { + if(this.map.hasLayer(this.marker)) + this.map.removeLayer(this.marker); + var location = e.latlng; + this.marker = L.marker(location, { draggable: false}).addTo(this.map); + + $("input[name=location_lat]").val(location.lat); + $("input[name=location_lon]").val(location.lng); + localStorage.setItem('lat', location.lat); + localStorage.setItem('lng', location.lng); + }; + +$(function () + { + new IssueMap(); + }); +})(); + + + diff --git a/client/apps/city_issues/static/city_issues/js/main.js b/client/apps/city_issues/static/city_issues/js/main.js index 70b2248..3e18c6f 100644 --- a/client/apps/city_issues/static/city_issues/js/main.js +++ b/client/apps/city_issues/static/city_issues/js/main.js @@ -14,7 +14,12 @@ function IssueMap(elementId) { }; IssueMap.prototype.setViewPoint = function(latitude, longitude, scale) { - this.map.setView([latitude, longitude], scale); + if (localStorage.getItem('lat') && localStorage.getItem('lng')) { + this.map.setView([localStorage.getItem('lat'), localStorage.getItem('lng')], scale); + } else { + this.map.setView([latitude, longitude], scale); + } + }; @@ -44,8 +49,9 @@ function IssueMap(elementId) { jsonData.forEach(function(key) { var issue = JSON.parse(key); - var marker = L.marker([issue.fields.location_lat, - issue.fields.location_lon],{icon: current.iconCreate(issue.fields.category)}); + var marker = L.marker( + [issue.fields.location_lat, issue.fields.location_lon], + {icon: current.iconCreate(issue.fields.category), title: issue.fields.title}); current.markers.addLayer(marker); current.getMap().addLayer(current.markers); @@ -83,8 +89,17 @@ function IssueMap(elementId) { }; + IssueMap.prototype.lanlatHendler = function(event) { + if (event.originalEvent.target.classList.contains("leaflet-marker-icon")) { + localStorage.setItem('lat', event.latlng.lat); + localStorage.setItem('lng', event.latlng.lng); + } + + }; + IssueMap.prototype.addHandler = function() { current.filterFormBtn.addEventListener('click', current.filterHandler); + current.map.on('click', current.lanlatHendler); }; IssueMap.prototype.getMarkers = function(serverURL) { @@ -120,6 +135,7 @@ function IssueDescription(mapId, issueContainerId, issueCloseId) { if (event.target.id.slice(0,16) == "issue_primary-id") { current.getIssueById(event.target.id.slice(16)); current.issue_box.style.display = 'block'; + console.log(event); } else { if (event.target.id == "mapid" && current.issue_box.style.display == "block") { diff --git a/client/apps/city_issues/templates/edit_issue.html b/client/apps/city_issues/templates/edit_issue.html index d2bdf42..8848f9b 100644 --- a/client/apps/city_issues/templates/edit_issue.html +++ b/client/apps/city_issues/templates/edit_issue.html @@ -1,17 +1,39 @@ {% extends "base.html" %} {% block meta_title %}Edit issue page{% endblock meta_title %} + +{% block extra_css %} + {% load static %} + + +{% endblock extra_css %} + {% block content %}
    -
    - {% csrf_token %} + {% csrf_token %} {{ form.as_p }} - +
    +
    +
    +
    -{% endblock content %} \ No newline at end of file +{% endblock content %} + +{% block extra_js %} + + + {% load static %} + + +{% endblock extra_js %} \ No newline at end of file diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index ac67861..7cb8feb 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -14,9 +14,10 @@ from django.urls import reverse from django.utils.timezone import make_aware from django.views.generic import CreateView, ListView +from django.views.generic.edit import UpdateView from city_issues.models import Attachments, Issues, IssueHistory, User -from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm +from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, IssueFormEdit class HomePageView(TemplateView): @@ -26,6 +27,7 @@ class HomePageView(TemplateView): class UserProfileView(View): """User profile page""" + def get(self, request, user_id): user = User.objects.get(id=user_id) user_issues = Issues.objects.filter(user_id=user_id) @@ -81,21 +83,6 @@ def map_page_view(request): return render(request, 'map_page.html', {'form': form}) -def edit_issue_view(request, issue_id): - """Edit page""" - if request.method == 'POST': - form = EditIssue(request.POST) - if form.is_valid(): - EditIssue(request.POST, instance=Issues.objects.get( - pk=issue_id)).save() - return redirect(reverse('map')) - else: - form = EditIssue(instance=Issues.objects.get(pk=issue_id)) - - return render( - request, 'edit_issue.html', {'form': form, 'issue_id': issue_id}) - - def get_issue_data(request, issue_id): """Returns single issue record as json""" @@ -187,3 +174,11 @@ def get_context_data(self, **kwargs): context = super(CheckIssues, self).get_context_data(**kwargs) context['issues_range'] = range(context["paginator"].num_pages) return context + + +class UpdateIssue(UpdateView): + """Edit issue from map.""" + model = Issues + form_class = IssueFormEdit + template_name = 'edit_issue.html' + success_url = '/map/' diff --git a/client/basic/urls.py b/client/basic/urls.py index 678df8b..213d669 100644 --- a/client/basic/urls.py +++ b/client/basic/urls.py @@ -21,8 +21,8 @@ from django.views.generic import RedirectView from city_issues.views import ( - CheckIssues, edit_issue_view, get_all_issues_data, get_issue_data, - HomePageView, map_page_view, IssueCreate, UserProfileView) + CheckIssues, get_all_issues_data, get_issue_data, + HomePageView, map_page_view, IssueCreate, UserProfileView, UpdateIssue) urlpatterns = [ @@ -35,7 +35,7 @@ get_issue_data, name='issue_data'), url(r'^map/getissuesall/$', get_all_issues_data, name='all_issues'), url(r'^add-issue', login_required(IssueCreate.as_view()), name='create_issue'), - url(r'^editissue/(?P[0-9]+)$', edit_issue_view, name='edit_issue'), + url(r'^editissue/(?P[0-9]+)$', UpdateIssue.as_view(), name='edit_issue'), # registration and authorization views url(r'^accounts/logout/$', auth_views.logout, kwargs={'next_page': 'home'}, name='auth_logout'), From 2d8b1a47f228514af22d1224634f52305850f4cf Mon Sep 17 00:00:00 2001 From: oksana-pidhurska Date: Sun, 3 Dec 2017 15:23:55 -0500 Subject: [PATCH 176/385] add relations --- backend/models/issues.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/models/issues.py b/backend/models/issues.py index 0f28a2e..13021d6 100644 --- a/backend/models/issues.py +++ b/backend/models/issues.py @@ -101,6 +101,8 @@ class Comments(db.Model): date_public = db.Column(db.TIMESTAMP) user_id = db.Column(db.ForeignKey(u'users.id')) issue_id = db.Column(db.ForeignKey(u'issues.id'), index=True) + issue = db.relationship(u'Issue') + user = db.relationship(u'User') class Meta: """...""" From 2ce36f603dc7b42e6bf8516fadf36f710b9dbf6d Mon Sep 17 00:00:00 2001 From: oksana-pidhurska Date: Sun, 3 Dec 2017 15:36:08 -0500 Subject: [PATCH 177/385] fix pep8 --- backend/forms/forms.py | 2 +- backend/views/views.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/forms/forms.py b/backend/forms/forms.py index 7eace4b..843d82e 100644 --- a/backend/forms/forms.py +++ b/backend/forms/forms.py @@ -274,7 +274,7 @@ class SearchIssuesForm(BaseForm): order_by = SelectField( 'order_by', choices=[ - ('0', 'title'), + ('0', 'summary'), ('1', 'category'), ] ) diff --git a/backend/views/views.py b/backend/views/views.py index e83f7b0..a7b49ce 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -202,14 +202,12 @@ def issues_page(): form = SearchIssuesForm(request.args, meta={'csrf': False}) condition = None order = None - if form.validate(): search_by = int(request.args.get('search_by')) order_by = int(request.args.get('order_by')) search_string = str(request.args.get('search')) search_list = ['title', 'category', 'description'] - if len(search_string) >= MIN_SEARCH_STR: search_parameter = '%{}%'.format(search_string) if search_list[search_by] == 'title': From 9b1b3e35befa2ce8bd0e2c191561614845c6102a Mon Sep 17 00:00:00 2001 From: Andrii Sereda Date: Thu, 30 Nov 2017 22:59:18 +0200 Subject: [PATCH 178/385] fixes --- backend/insert_db_data.py | 1 + backend/views/views.py | 1 + client/apps/city_issues/forms/forms.py | 57 +++++++++++++++++++ client/apps/city_issues/models/issues.py | 28 ++++----- .../apps/city_issues/static/css/general.css | 36 ++++++++++++ client/apps/city_issues/static/css/issues.css | 32 ----------- client/apps/city_issues/static/js/general.js | 10 ++++ client/apps/city_issues/static/js/issues.js | 11 ---- client/apps/city_issues/templates/base.html | 12 +++- .../apps/city_issues/templates/map_page.html | 20 ++++++- .../apps/city_issues/templates/user/user.html | 57 ++++++++++++++++--- client/apps/city_issues/views.py | 40 ++++++++++--- client/basic/urls.py | 5 +- 13 files changed, 228 insertions(+), 82 deletions(-) create mode 100644 client/apps/city_issues/static/css/general.css create mode 100644 client/apps/city_issues/static/js/general.js diff --git a/backend/insert_db_data.py b/backend/insert_db_data.py index 71d9941..616b817 100644 --- a/backend/insert_db_data.py +++ b/backend/insert_db_data.py @@ -33,6 +33,7 @@ user3 = User(name='Maria', alias='Mary', email='maria@gmail.com', password='321', role_id='3') + issue1 = Issue(title='Road accident', user_id='2', category_id='1', location_lat='50.621945', location_lon='26.249314', description='Car crash ...', status='on moderation', open_date='2017/11/15') diff --git a/backend/views/views.py b/backend/views/views.py index 1c8ea4a..75bd9f7 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -247,6 +247,7 @@ def issue_modify(issue_id): return render_template('issue_modify.html', form=form, route_to=route_to) + @app.route('/deleteissue/', methods=['POST']) @admin_permissions def delete_issue(issue_id): diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index c928b47..00394fe 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -7,6 +7,7 @@ Textarea, TextInput) from city_issues.models.issues import Issues, Category +from city_issues.models.users import User class IssueForm(forms.ModelForm): @@ -88,3 +89,59 @@ class IssueFilter(forms.Form): date_to = forms.DateField( required=True, widget=DateInput(attrs={'type': 'date'})) + + +class EditUserForm(forms.ModelForm): + + class Meta: + model = User + fields = ['name', 'alias', 'email'] + + def clean_email(self): + # Get the email + email = self.cleaned_data.get('email') + + # Check to see if any users already exist with this email as a username. + try: + match = User.objects.get(email=email) + except User.DoesNotExist: + # Unable to find a user, this is fine + return email + + # A user was found with this as a username, raise an error. + raise forms.ValidationError('This email address is already in use.') + + name = forms.CharField( + max_length=25, + min_length=3, + widget=forms.TextInput(attrs={'class': 'form-control'}), + ) + + alias = forms.CharField( + max_length=20, + min_length=3, + widget=forms.TextInput(attrs={'class': 'form-control'}), + ) + + email = forms.EmailField( + max_length=50, + min_length=4, + widget=forms.EmailInput(attrs={'class': 'form-control'}), + ) + + current_password = forms.CharField( + widget=forms.PasswordInput(attrs={'class': 'form-control'}) + ) + + new_password = forms.CharField( + max_length=50, + min_length=4, + widget=forms.PasswordInput(attrs={'class': 'form-control'}) + ) + + confirm_password = forms.CharField( + max_length=50, + min_length=4, + widget=forms.PasswordInput(attrs={'class': 'form-control'}) + ) + diff --git a/client/apps/city_issues/models/issues.py b/client/apps/city_issues/models/issues.py index c50b49b..e5576e3 100644 --- a/client/apps/city_issues/models/issues.py +++ b/client/apps/city_issues/models/issues.py @@ -47,11 +47,24 @@ class Meta: db_table = 'category' +class Statuses(models.Model): + """ + Status table in the database. + """ + status = models.TextField(blank=True, null=True) + + class Meta: + """...""" + app_label = 'city_issues' + managed = False + db_table = 'statuses' + + class IssueHistory(models.Model): """ IssueHistory table in the database. """ - STATUS_ID_NEW = 1 + STATUS_ID_NEW = Statuses.objects.get(id=1) user = models.ForeignKey('User', models.DO_NOTHING, blank=True, null=True) @@ -90,16 +103,3 @@ class Meta: app_label = 'city_issues' managed = False db_table = 'issues' - - -class Statuses(models.Model): - """ - Status table in the database. - """ - status = models.TextField(blank=True, null=True) - - class Meta: - """...""" - app_label = 'city_issues' - managed = False - db_table = 'statuses' diff --git a/client/apps/city_issues/static/css/general.css b/client/apps/city_issues/static/css/general.css new file mode 100644 index 0000000..d22811c --- /dev/null +++ b/client/apps/city_issues/static/css/general.css @@ -0,0 +1,36 @@ + +li.success, li.error { + float: right; + font-size: 18px; + font-weight: bold; +} + +li.success{ + color: green; +} + +li.error{ + color: red; +} + +ul.messages{ + list-style-type: none; + position: absolute; + top: 75px; + z-index: 1000; + right: 90px; +} + +a.del-msg{ + float: right; +} + +.wrapper{ + margin: 0 100px; + min-width: 1100px; +} + +.width{ + width: 100%; + margin: 20px 0; +} \ No newline at end of file diff --git a/client/apps/city_issues/static/css/issues.css b/client/apps/city_issues/static/css/issues.css index 9647a72..3f3552d 100644 --- a/client/apps/city_issues/static/css/issues.css +++ b/client/apps/city_issues/static/css/issues.css @@ -1,13 +1,3 @@ -.wrapper{ - margin: 0 100px; - min-width: 1100px; -} - -.width{ - width: 100%; - margin: 20px 0; -} - #map{ height: 600px; width: 100%; @@ -77,29 +67,7 @@ opacity: 0; } -li.success, li.error { - float: right; - font-size: 18px; - font-weight: bold; -} - -li.success{ - color: green; -} - -li.error{ - color: red; -} - .form-group.required .control-label:after { content:"*"; color:red; } - -ul.messages{ - list-style-type: none; -} - -a.del-msg{ - float: right; -} diff --git a/client/apps/city_issues/static/js/general.js b/client/apps/city_issues/static/js/general.js new file mode 100644 index 0000000..6f4bc4a --- /dev/null +++ b/client/apps/city_issues/static/js/general.js @@ -0,0 +1,10 @@ +setTimeout(function () +{ + $('.messages').fadeOut('slow'); +}, 10000); + +$('.del-msg').on('click', function (e) +{ + e.preventDefault(); + $('.del-msg').parent().hide(); +}); \ No newline at end of file diff --git a/client/apps/city_issues/static/js/issues.js b/client/apps/city_issues/static/js/issues.js index e63ec3e..ea02ccb 100644 --- a/client/apps/city_issues/static/js/issues.js +++ b/client/apps/city_issues/static/js/issues.js @@ -22,15 +22,4 @@ IssueMap.prototype.getLocation = function (e) $(function () { new IssueMap(); - - setTimeout(function() - { - $('.messages').fadeOut('slow'); - }, 10000); - - $('.del-msg').on('click',function(e) - { - e.preventDefault(); - $('.del-msg').parent().hide(); - }) }); diff --git a/client/apps/city_issues/templates/base.html b/client/apps/city_issues/templates/base.html index 0d99031..2e1997e 100644 --- a/client/apps/city_issues/templates/base.html +++ b/client/apps/city_issues/templates/base.html @@ -14,7 +14,11 @@ href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> - {% block extra_css %}{% endblock extra_css %} + {% load static %} + + {% block extra_css %} + + {% endblock extra_css %} @@ -30,7 +34,7 @@ id="bs-example-navbar-collapse-1"> {% if user.is_authenticated %} @@ -39,7 +43,7 @@ Log out + href="{% url 'user_profile' %}">{{ user.email }}

    {% else %} Log in @@ -61,6 +65,8 @@ +{% load static %} + {% block extra_js %}{% endblock extra_js %} diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index 3e78996..6b2ca34 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -17,8 +17,24 @@
    - - + +
    +
    +
    + {% if messages %} +
      + {% for message in messages %} + + {{ message }} + × + + {% endfor %} +
    + {% endif %} +
    +
    +
    diff --git a/client/apps/city_issues/templates/user/user.html b/client/apps/city_issues/templates/user/user.html index d7360ea..e64b4c2 100644 --- a/client/apps/city_issues/templates/user/user.html +++ b/client/apps/city_issues/templates/user/user.html @@ -15,6 +15,18 @@ {% block content %}
    +
    + {% if messages %} +
      + {% for message in messages %} + + {{ message }} + × + + {% endfor %} +
    + {% endif %} +

    {{ user.name }}

    @@ -50,10 +62,10 @@

    {{ user.name }}

    - {% for isssue in user_issues %} + {% for issue in user_issues %} - - - - - + + + + + - @@ -38,7 +37,6 @@

    {{ object.title }} {{ object.user }}

    - From 88fb9f439a7f3d6b82aef5c88e07f120b164dba7 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 6 Dec 2017 18:33:04 +0200 Subject: [PATCH 194/385] small issue img fix --- client/apps/city_issues/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index ef4b976..0c3d2c4 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -101,8 +101,6 @@ def get_issue_data(request, issue_id): for img in images_urls: if img and os.path.isfile(os.path.join(settings.MEDIA_ROOT, img)): checked_img_urls.append(img) - else: - checked_img_urls.append(None) issue_query = list(Issues.objects.filter(pk=issue_id).values()) issue_dict = issue_query[0] From caf5d8de958da90c5c2f72adbfe2227b6a77e3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9DAlex?= Date: Wed, 6 Dec 2017 18:35:20 +0200 Subject: [PATCH 195/385] fix a link bug in base_html --- client/apps/city_issues/templates/base.html | 2 +- client/apps/city_issues/templates/issue_detailed.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/apps/city_issues/templates/base.html b/client/apps/city_issues/templates/base.html index 7a595d4..1b8a254 100644 --- a/client/apps/city_issues/templates/base.html +++ b/client/apps/city_issues/templates/base.html @@ -46,7 +46,7 @@ class="navbar-text navbar-right">Log in Sign in - All Issues {% endif %} diff --git a/client/apps/city_issues/templates/issue_detailed.html b/client/apps/city_issues/templates/issue_detailed.html index de94a07..d31e2a3 100644 --- a/client/apps/city_issues/templates/issue_detailed.html +++ b/client/apps/city_issues/templates/issue_detailed.html @@ -45,4 +45,4 @@

    {{ object.title }} {{ object.user }}

    -{% endblock content %} \ No newline at end of file +{% endblock content %} From c9ec970b3338a4b690b1ce17688ea2047cb11db7 Mon Sep 17 00:00:00 2001 From: Andrii Sereda Date: Wed, 6 Dec 2017 21:02:18 +0200 Subject: [PATCH 196/385] fix saving password on user page --- client/apps/city_issues/forms/forms.py | 12 +----------- .../apps/city_issues/static/css/general.css | 1 + client/apps/city_issues/templates/base.html | 4 ---- .../city_issues/templates/issues/issues.html | 1 + .../apps/city_issues/templates/map_page.html | 2 ++ client/apps/city_issues/views.py | 19 +++++++++---------- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index 1549658..b04de67 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -92,20 +92,10 @@ class IssueFilter(forms.Form): class EditUserForm(forms.ModelForm): - + """Edit user form""" class Meta: model = User fields = ['name', 'alias', 'email'] - # - # def clean_email(self): - # alias = self.cleaned_data["alias"] - # email = self.cleaned_data["email"] - # users = User.objects.filter(email__iexact=email).exclude( - # alias__iexact=alias) - # if users: - # raise forms.ValidationError( - # 'User with that email already exists.') - # return email.lower() name = forms.CharField( max_length=25, diff --git a/client/apps/city_issues/static/css/general.css b/client/apps/city_issues/static/css/general.css index d22811c..0237b3b 100644 --- a/client/apps/city_issues/static/css/general.css +++ b/client/apps/city_issues/static/css/general.css @@ -19,6 +19,7 @@ ul.messages{ top: 75px; z-index: 1000; right: 90px; + background: white; } a.del-msg{ diff --git a/client/apps/city_issues/templates/base.html b/client/apps/city_issues/templates/base.html index 2e1997e..1b020c0 100644 --- a/client/apps/city_issues/templates/base.html +++ b/client/apps/city_issues/templates/base.html @@ -14,8 +14,6 @@ href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> - {% load static %} - {% block extra_css %} {% endblock extra_css %} @@ -65,8 +63,6 @@ -{% load static %} - {% block extra_js %}{% endblock extra_js %} diff --git a/client/apps/city_issues/templates/issues/issues.html b/client/apps/city_issues/templates/issues/issues.html index bd2fc70..798f98e 100644 --- a/client/apps/city_issues/templates/issues/issues.html +++ b/client/apps/city_issues/templates/issues/issues.html @@ -7,6 +7,7 @@ {% block extra_css %} {% load static %} + diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index 6b2ca34..1acf649 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -10,6 +10,7 @@ crossorigin=""/> {% load static %} + {% endblock extra_css %} {% block content %} @@ -89,4 +90,5 @@

    Issue description

    crossorigin=""> {% load static %} + {% endblock extra_js %} diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index 107a0a2..0efe248 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -5,15 +5,15 @@ import json from datetime import date, datetime, time -from django.views.generic import CreateView, UpdateView +from django.contrib.auth import update_session_auth_hash +from django.views.generic import CreateView from django.views.generic.base import TemplateView, View from django.contrib import messages from django.core import serializers -from django.http import JsonResponse, HttpResponse +from django.http import JsonResponse from django.shortcuts import redirect, render from django.urls import reverse from django.utils.timezone import make_aware -from passlib.handlers.django import django_bcrypt from city_issues.models import Attachments, Issues, IssueHistory, User from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, EditUserForm @@ -31,9 +31,9 @@ class UserProfileView(View): template_name = 'user/user.html' def get(self, request): - user = User.objects.get(id=request.user.id) + user = request.user user_issues = Issues.objects.filter(user_id=user.id) - form = self.form_class(instance=User.objects.get(pk=user.id)) + form = self.form_class(instance=User.objects.get(id=user.id)) return render(request, self.template_name, {'user': user, 'user_issues': user_issues, @@ -44,15 +44,15 @@ def post(self, request): form = EditUserForm(data=request.POST, instance=request.user) if form.is_valid(): - print user.id - print user.name - if self.is_not_empty_passwrds(form.cleaned_data) and self.check_passwords(user, form.cleaned_data): + if self.is_not_empty_passwords(form.cleaned_data) and self.check_passwords(user, form.cleaned_data): + user.set_password(form.cleaned_data['confirm_password']) user.name = form.cleaned_data['name'] user.alias = form.cleaned_data['alias'] user.email = form.cleaned_data['email'] user.save() + update_session_auth_hash(request, user) messages.success(request, 'Changes successfully saved') else: @@ -61,7 +61,6 @@ def post(self, request): return redirect(self.success_url) def check_passwords(self, user, form_data): - print user.id if not user.check_password(form_data['current_password']): messages.error(self.request, 'Wrong current password') return redirect(self.success_url) @@ -73,7 +72,7 @@ def check_passwords(self, user, form_data): else: return True - def is_not_empty_passwrds(self, data): + def is_not_empty_passwords(self, data): if data['current_password'] == '' and data['new_password'] == '' and data['confirm_password'] == '': return False else: From bf8330e789f02101655948dd014945d22e44844f Mon Sep 17 00:00:00 2001 From: Andrii Sereda Date: Wed, 6 Dec 2017 21:43:36 +0200 Subject: [PATCH 197/385] fix --- backend/views/views.py | 1 + client/apps/city_issues/views.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/views/views.py b/backend/views/views.py index 8f700fa..a7b49ce 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -255,6 +255,7 @@ def issue_modify(issue_id): return render_template('issue_modify.html', form=form, route_to=route_to) + @app.route('/deleteissue/', methods=['POST']) @admin_permissions def delete_issue(issue_id): diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index 571dd3a..9067666 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -22,7 +22,6 @@ from django.views.generic.edit import UpdateView from city_issues.models import Attachments, Issues, IssueHistory, User -from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, IssueFormEdit, EditUserForm ROLE_ADMIN = 1 @@ -30,7 +29,6 @@ ROLE_USER = 3 - class HomePageView(TemplateView): """Home page""" template_name = "home_page.html" From c36e1055bec78833be8123f4c43480a4d13257b9 Mon Sep 17 00:00:00 2001 From: Andrii Sereda Date: Wed, 6 Dec 2017 21:50:44 +0200 Subject: [PATCH 198/385] pylint fixes --- client/apps/city_issues/forms/forms.py | 15 ++++++--------- client/apps/city_issues/views.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index 192da17..dedc0a5 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -3,7 +3,6 @@ from django import forms from city_issues.models.issues import Issues, Category -from city_issues.models.users import User class IssueForm(forms.ModelForm): @@ -84,8 +83,6 @@ class IssueFilter(forms.Form): widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})) date_to = forms.DateField( - required=True, - widget=DateInput(attrs={'type': 'date'})) required=False, widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})) @@ -109,11 +106,6 @@ class IssueFilter(forms.Form): required=False,) -class IssueFormEdit(IssueForm): - - files = None - - class EditUserForm(forms.ModelForm): """Edit user form""" class Meta: @@ -156,4 +148,9 @@ class Meta: max_length=50, min_length=4, widget=forms.PasswordInput(attrs={'class': 'form-control'}) - ) \ No newline at end of file + ) + + +class IssueFormEdit(IssueForm): + + files = None diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index 9067666..92be3fd 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -70,7 +70,7 @@ def post(self, request): return redirect(self.success_url) - def check_passwords(self, user, form_data): + def check_passwords(self, user, form_data): if not user.check_password(form_data['current_password']): messages.error(self.request, 'Wrong current password') return redirect(self.success_url) From d5c7a292b1a0917e3502b2e06e845af6d18cdd36 Mon Sep 17 00:00:00 2001 From: Andrii Sereda Date: Wed, 6 Dec 2017 22:23:59 +0200 Subject: [PATCH 199/385] fix in forms --- client/apps/city_issues/forms/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index dedc0a5..61eb6a4 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -3,6 +3,7 @@ from django import forms from city_issues.models.issues import Issues, Category +from city_issues.models.users import User class IssueForm(forms.ModelForm): From 80dc4f0078e5395e258efc769207350e873c2e41 Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Wed, 6 Dec 2017 23:35:58 +0200 Subject: [PATCH 200/385] fixed according recommendation --- client/apps/city_issues/forms/forms.py | 4 +-- .../static/city_issues/js/search_issue.js | 30 +++++++++++++++++++ .../city_issues/templates/issues_list.html | 4 +-- 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 client/apps/city_issues/static/city_issues/js/search_issue.js diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index a545254..f9fd314 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -107,7 +107,7 @@ class IssueFilter(forms.Form): class IssueSearchForm(forms.Form): - "Issue search form" + """Issue search form""" search = forms.CharField( min_length=2, max_length=100, @@ -117,11 +117,9 @@ class IssueSearchForm(forms.Form): }), required=False) order_by = forms.CharField( - max_length=15, widget=forms.HiddenInput(), required=False) reverse = forms.CharField( - max_length=3, widget=forms.HiddenInput(), required=False) page = forms.IntegerField( diff --git a/client/apps/city_issues/static/city_issues/js/search_issue.js b/client/apps/city_issues/static/city_issues/js/search_issue.js new file mode 100644 index 0000000..92e2944 --- /dev/null +++ b/client/apps/city_issues/static/city_issues/js/search_issue.js @@ -0,0 +1,30 @@ +$( document ).ready(function() { + $('.get-request').on('click', function(e) { + e.preventDefault(); + + var search = $('#search-filter').data('search'); + var order_by = $(this).data('order-by'); + var reverse = $(this).data('reverse'); + var page = $(this).data('page'); + + if (order_by === undefined){ + order_by = $('#search-filter').data('order'); + } + if (reverse === undefined){ + reverse = $('#search-filter').data('reverse'); + } + + $("#id_search").val(search); + $("#id_order_by").val(order_by); + $("#id_reverse").val(reverse); + $("#id_page").val(page); + + $('#search-filter').data('order',order_by); + $('#submit-btn').click(); + }); + + $('#search-filter').on('submit', function(e){ + order_by = $('#search-filter').data('order'); + $("#id_order_by").val(order_by); + }); +}); \ No newline at end of file diff --git a/client/apps/city_issues/templates/issues_list.html b/client/apps/city_issues/templates/issues_list.html index 2ac6075..0a822a2 100644 --- a/client/apps/city_issues/templates/issues_list.html +++ b/client/apps/city_issues/templates/issues_list.html @@ -15,7 +15,7 @@

    All Issues:

    {{form}} - + {% endwith %} @@ -115,5 +115,5 @@

    All Issues:

    {% block extra_js %} {% load static %} - + {% endblock extra_js %} From a59d75a93e6266934d9d96c3f2936457b5ec4af5 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 6 Dec 2017 23:56:03 +0200 Subject: [PATCH 201/385] /map-UI-fixes --- .../city_issues/static/city_issues/css/style.css | 15 ++++++++------- client/apps/city_issues/templates/map_page.html | 2 +- client/apps/city_issues/views.py | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/client/apps/city_issues/static/city_issues/css/style.css b/client/apps/city_issues/static/city_issues/css/style.css index 8840e2e..ab924e3 100644 --- a/client/apps/city_issues/static/city_issues/css/style.css +++ b/client/apps/city_issues/static/city_issues/css/style.css @@ -4,7 +4,7 @@ .issue_container { position: fixed; - top: 0; + top: 72px; right: 0; display: none; @@ -19,12 +19,13 @@ } .issue_img-box { + width: 360px; height: 225px; } -.issue_img { - height: 225px; - width: auto; +#carousel-example-generic img { + height: 100%; + width: 100%; } .carousel-inner .item{ @@ -46,17 +47,17 @@ z-index: 999; } -#message_box { +.message_box { display: none; } -#message_box ul { +.message_box ul { list-style: none; margin: 0; padding: 0; } -#message_box .error { +.message_box .error { font-size: 16px; color: red; diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index 8072bae..10cec3c 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -17,7 +17,7 @@
    -

    +

    {% csrf_token %} {% for field in form %}
    diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index 5564df8..dea6671 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -131,9 +131,9 @@ def get_all_issues_data(request): category = form.data.get('category') search = form.cleaned_data.get('search') - show_closed = not show_closed + show_opened_issue = not show_closed - kwargs = {"close_date__isnull": (show_closed)} + kwargs = {"close_date__isnull": (show_opened_issue)} if map_date_from and map_date_to: date_from = make_aware(datetime.combine(map_date_from, time.min)) From f02ba6e70010aa27ddb7eb40bd30af4cda43b913 Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Thu, 7 Dec 2017 00:15:15 +0200 Subject: [PATCH 202/385] fix pep8 --- client/apps/city_issues/forms/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index f9fd314..201f263 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -125,8 +125,8 @@ class IssueSearchForm(forms.Form): page = forms.IntegerField( widget=forms.HiddenInput(), required=False) - - + + class IssueFormEdit(IssueForm): files = None From d9c8f71ae9af1e8b24c1b70997773eee191d1ef9 Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Thu, 7 Dec 2017 09:06:39 +0200 Subject: [PATCH 203/385] add comments, some fix --- backend/insert_db_data.py | 33 ++++++++++++------- backend/models/issues.py | 1 + backend/templates/issue.html | 20 ++++++----- backend/views/views.py | 16 ++++++--- client/apps/city_issues/forms/forms.py | 4 +-- .../city_issues/templates/comment_issue.html | 4 +-- .../city_issues/templates/issue_detailed.html | 2 +- .../city_issues/templates/issues_list.html | 2 +- client/apps/city_issues/views.py | 4 +-- 9 files changed, 54 insertions(+), 32 deletions(-) diff --git a/backend/insert_db_data.py b/backend/insert_db_data.py index 616b817..d15cc70 100644 --- a/backend/insert_db_data.py +++ b/backend/insert_db_data.py @@ -5,7 +5,7 @@ from backend.app import db from backend.config import Config from backend.models.users import Role, User -from backend.models.issues import (Attachment, Category, +from backend.models.issues import (Attachment, Category, Comments, Issue, IssueHistory, Status) db_credentials = Config.SQLALCHEMY_DATABASE_URI @@ -34,29 +34,29 @@ password='321', role_id='3') -issue1 = Issue(title='Road accident', user_id='2', category_id='1', location_lat='50.621945', - location_lon='26.249314', description='Car crash ...', status='on moderation', +issue1 = Issue(title='First issue', user_id='2', category_id='1', location_lat='50.621945', + location_lon='26.249314', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='on moderation', open_date='2017/11/15') issue2 = Issue(title='Road accident', user_id='3', category_id='2', location_lat='50.623673', - location_lon='26.250182', description='Bus crash ...', status='new', + location_lon='26.250182', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='new', open_date='2017/11/16') issue3 = Issue(title='Dog lost', user_id='3', category_id='3', location_lat='50.622584', - location_lon='26.252468', description='Some test data 3', status='new', + location_lon='26.252468', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='new', open_date='2017/11/17') issue4 = Issue(title='Road accident', user_id='1', category_id='1', location_lat='50.622836', - location_lon='26.246706', description='Some test data 4', status='new', + location_lon='26.246706', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='new', open_date='2017/11/18') issue5 = Issue(title='Test case 5', user_id='2', category_id='2', location_lat='50.619977', - location_lon='26.247521', description='Some test data 5', status='open', + location_lon='26.247521', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='open', open_date='2017/11/19') issue6 = Issue(title='Test case 6', user_id='3', category_id='3', location_lat='50.619453', - location_lon='26.251834', description='Some test data 6', status='open', + location_lon='26.251834', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='open', open_date='2017/11/20') issue7 = Issue(title='Test case 7', user_id='1', category_id='1', location_lat='50.618500', - location_lon='26.249838', description='Some test data 7', status='on moderation', + location_lon='26.249838', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='on moderation', open_date='2017/11/21') issue8 = Issue(title='Test case 8', user_id='2', category_id='2', location_lat='50.619242', - location_lon='26.246233', description='Some test data 8', status='closed', + location_lon='26.246233', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='closed', open_date='2017/11/22') @@ -64,8 +64,9 @@ attachment2 = Attachment(issue_id='2', image_url=None) attachment3 = Attachment(issue_id='3', image_url=None) + issueHistory1 = IssueHistory(user_id='1', issue_id='1', status_id='1', - transaction_date='2017/10/25') + transaction_date='2017/09/25') issueHistory2 = IssueHistory(user_id='1', issue_id='1', status_id='2', transaction_date='2017/10/27') issueHistory3 = IssueHistory(user_id='3', issue_id='3', status_id='1', @@ -80,6 +81,13 @@ transaction_date='2017/11/17') +comment1 = Comments(user_id='1', issue_id='1', date_public='2017/09/26', comment='Vestibulum et scelerisque felis. Nunc elementum commodo enim non maximus. Vestibulum sed molestie sem. Maecenas aliquam at eros at mattis. Maecenas vitae suscipit turpis, at iaculis sem. Donec sagittis mauris sapien. Duis ultricies odio ac libero tincidunt convallis eget non dui. Quisque condimentum vel justo eget ultrices. In efficitur laoreet velit. ') +comment2 = Comments(user_id='2', issue_id='1', date_public='2017/10/10', comment='Nunc elementum commodo enim non maximus. Vestibulum sed molestie sem. Maecenas aliquam at eros at mattis. Maecenas vitae suscipit turpis, at iaculis sem. Donec sagittis mauris sapien. Duis ultricies odio ac libero tincidunt convallis eget non dui. Quisque condimentum vel justo eget ultrices. In efficitur laoreet velit. ') +comment3 = Comments(user_id='3', issue_id='1', date_public='2017/11/06', comment='Maecenas vitae suscipit turpis, at iaculis sem. Donec sagittis mauris sapien. Duis ultricies odio ac libero tincidunt convallis eget non dui. Quisque condimentum vel justo eget ultrices. In efficitur laoreet velit. ') +comment4 = Comments(user_id='2', issue_id='3', date_public='2017/10/16', comment='Vivamus mauris massa, ullamcorper vel faucibus vitae, mollis at dolor. Vivamus fringilla ex ac pretium rhoncus. Integer dictum ligula est, vitae dictum metus condimentum tempus.') +comment5 = Comments(user_id='3', issue_id='3', date_public='2017/10/16', comment='Sed bibendum arcu eu nisi vestibulum, consectetur hendrerit mi pharetra. Vivamus mauris massa, ullamcorper vel faucibus vitae, mollis at dolor. Vivamus fringilla ex ac pretium rhoncus. Integer dictum ligula est, vitae dictum metus condimentum tempus.') + + def db_insert_data(): """This function insert database data""" db.session.add_all([role, role1, role2, @@ -92,7 +100,8 @@ def db_insert_data(): issueHistory1, issueHistory2, issueHistory3, issueHistory4, issueHistory5, issueHistory6, issueHistory7, - attachment1, attachment2, attachment3]) + attachment1, attachment2, attachment3, + comment1, comment2, comment3, comment4, comment5]) db.session.commit() print "Test data has been inserted into the database" diff --git a/backend/models/issues.py b/backend/models/issues.py index 13021d6..724b3b5 100644 --- a/backend/models/issues.py +++ b/backend/models/issues.py @@ -101,6 +101,7 @@ class Comments(db.Model): date_public = db.Column(db.TIMESTAMP) user_id = db.Column(db.ForeignKey(u'users.id')) issue_id = db.Column(db.ForeignKey(u'issues.id'), index=True) + issue = db.relationship(u'Issue') user = db.relationship(u'User') diff --git a/backend/templates/issue.html b/backend/templates/issue.html index da7c26b..e116985 100644 --- a/backend/templates/issue.html +++ b/backend/templates/issue.html @@ -27,7 +27,7 @@

    -
    Desctiption:
    +
    Description:
    {{ issue.description }}
    Category:
    {{issue.category.category}}
    @@ -45,13 +45,17 @@ {% endif %}

    - {% if history_list %} -
      - {% for history in history_list%} -
    • {{ history.transaction_date.strftime('%Y-%m-%d')}} status changed to {{ history.status.status }} by {{ history.user.alias }}
    • - {% endfor %} -
    - {% endif %} +
      + {% for history in list_history%} + {% if history[0] == 'change_status' %} +
    • {{ history[3]}} status changed to {{ history[1] }} by {{ history[2] }}
    • + {% else %} +
        +
      • {{ history[3]}} by {{history[1]}}

        "{{history[2]}}"

      • +
      + {% endif%} + {% endfor %} +
    diff --git a/backend/views/views.py b/backend/views/views.py index a7b49ce..bfba75c 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -9,7 +9,7 @@ from backend.forms.forms import (IssueForm, LoginForm, SearchUserForm, SearchIssuesForm, UserForm, UserAddForm) -from backend.models.issues import (Attachment, Category, IssueHistory, +from backend.models.issues import (Attachment, Category, Comments, IssueHistory, Issue, Status) from backend.models.users import Role, User @@ -284,6 +284,14 @@ def issue_info(issue_id): attach = db.session.query(Attachment).filter(and_( Attachment.issue_id == issue_id)).all() history = db.session.query( - IssueHistory).filter(and_( - IssueHistory.issue_id == issue_id)).order_by(IssueHistory.transaction_date).all() - return render_template('issue.html', issue=issue_one, attachments=attach, history_list=history) + IssueHistory).filter(IssueHistory.issue_id == issue_id).order_by( + IssueHistory.transaction_date).all() + comments = db.session.query(Comments).filter( + Comments.issue_id == issue_id).order_by(Comments.date_public).all() + list_history = [] + for histor in history: + list_history.append(['change_status', histor.status.status, histor.user.alias, histor.transaction_date.strftime('%Y-%m-%d %H:%M')]) + for comment in comments: + list_history.append(['add_comment', comment.user.alias, comment.comment, comment.date_public.strftime('%Y-%m-%d %H:%M')]) + list_history.sort(key=lambda history: history[3]) + return render_template('issue.html', issue=issue_one, attachments=attach, list_history=list_history) diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index 201f263..56da0e1 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -1,6 +1,6 @@ """Forms models""" -from django.core.validators import MaxValueValidator, MinValueValidator from django import forms +from django.core.validators import MaxValueValidator, MinValueValidator from city_issues.models.issues import Issues, Category @@ -107,7 +107,7 @@ class IssueFilter(forms.Form): class IssueSearchForm(forms.Form): - """Issue search form""" + """Issue search form.""" search = forms.CharField( min_length=2, max_length=100, diff --git a/client/apps/city_issues/templates/comment_issue.html b/client/apps/city_issues/templates/comment_issue.html index c84403c..383ca20 100644 --- a/client/apps/city_issues/templates/comment_issue.html +++ b/client/apps/city_issues/templates/comment_issue.html @@ -34,9 +34,9 @@

    Comment issue

    TitleFirst NameLast NameSexStatus PersonCategotyStatusDescriptionOpen Date
    {{ issue.title }}
    {{ forloop.counter }} {{ isssue.issue.title }}{{ isssue.issue.category.category }}{{ isssue.issue.status }}{{ isssue.issue.open_date }}{{ isssue.issue.close_date }}{{ issue.title }}{{ issue.category.category }}{{ issue.status }}{{ issue.open_date }}{{ issue.close_date }} - edit @@ -110,6 +122,33 @@

    Added issus

    + + {% endblock content %} diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index c5c4588..b660340 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -5,17 +5,18 @@ import json from datetime import date, datetime, time -from django.views.generic import CreateView +from django.views.generic import CreateView, UpdateView from django.views.generic.base import TemplateView, View from django.contrib import messages from django.core import serializers -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse from django.shortcuts import redirect, render from django.urls import reverse from django.utils.timezone import make_aware +from passlib.handlers.django import django_bcrypt from city_issues.models import Attachments, Issues, IssueHistory, User -from city_issues.forms.forms import EditIssue, IssueForm +from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, EditUserForm class HomePageView(TemplateView): @@ -24,12 +25,35 @@ class HomePageView(TemplateView): class UserProfileView(View): + """User profile page""" + form_class = EditUserForm + success_url = 'user_profile' + template_name = 'user/user.html' + + def get(self, request): + user = User.objects.get(id=request.user.id) + user_issues = Issues.objects.filter(user_id=user.id) + form = self.form_class(instance=User.objects.get(pk=user.id)) + + return render(request, self.template_name, {'user': user, + 'user_issues': user_issues, + 'form': form}) + + def post(self, request): + if request.method == 'POST': + form = EditUserForm(request.POST, instance=request.user) + if form.is_valid(): + print form.cleaned_data + self.check_passwords(form.cleaned_data['current_password']) - def get(self, request, user_id): - user = User.objects.get(id=user_id) - user_issues = IssueHistory.objects.filter(user=user).select_related('issue') + else: + messages.error(request, form.errors) + + return redirect(self.success_url) - return render(request, 'user/user.html', {'user': user, 'user_issues': user_issues}) + def check_passwords(self, current_pass, new_pass, confirm_pass): + if not django_bcrypt.verify(current_pass, self.request.user.password): + return messages.error(self.request, 'Wrong password') class IssueCreate(CreateView): @@ -39,7 +63,7 @@ class IssueCreate(CreateView): model = Issues form_class = IssueForm template_name = 'issues/issues.html' - success_url = 'add-issue' + success_url = 'map' def form_valid(self, form): form.instance.user = self.request.user diff --git a/client/basic/urls.py b/client/basic/urls.py index 0745ce6..4244382 100644 --- a/client/basic/urls.py +++ b/client/basic/urls.py @@ -16,7 +16,6 @@ from django.contrib.auth import views as auth_views from django.conf import settings from django.conf.urls import include, url -from django.conf.urls import include, url from django.conf.urls.static import static from django.contrib.auth.decorators import login_required from django.views.generic import RedirectView @@ -37,7 +36,7 @@ # registration and authorization views url(r'^accounts/logout/$', auth_views.logout, kwargs={'next_page': 'home'}, name='auth_logout'), - url(r'^accounts/profile/(?P[0-9]+)$', UserProfileView.as_view(), name='user_profile'), - url(r'^accounts/', include('registration.backends.simple.urls', namespace='accounts')), + url(r'^accounts/profile/$', UserProfileView.as_view(), name='user_profile'), + url(r'^accounts/', include('registration.backends.simple.urls', namespace='accounts', )), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) From be9e0b14d6200a6b541b0ec847e77ee58d79e8ed Mon Sep 17 00:00:00 2001 From: Andrii Sereda Date: Sun, 3 Dec 2017 22:54:12 +0200 Subject: [PATCH 179/385] edit user profile --- client/apps/city_issues/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index b660340..e1d909f 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -51,7 +51,7 @@ def post(self, request): return redirect(self.success_url) - def check_passwords(self, current_pass, new_pass, confirm_pass): + def check_passwords(self, current_pass): if not django_bcrypt.verify(current_pass, self.request.user.password): return messages.error(self.request, 'Wrong password') From c439bdfaedd7eb89c9b484441ed0bea0c614874e Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Sun, 3 Dec 2017 20:51:53 +0200 Subject: [PATCH 180/385] demo search issue --- client/apps/city_issues/forms/forms.py | 1 - .../city_issues/templates/issues_list.html | 13 ++++++----- client/apps/city_issues/views.py | 22 ++++++++++++++++++- client/basic/urls.py | 6 ++--- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index bae72e6..b9d63bc 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -105,7 +105,6 @@ class IssueFilter(forms.Form): }), required=False,) - class IssueSearchForm(forms.Form): "Issue search form" search = forms.CharField( diff --git a/client/apps/city_issues/templates/issues_list.html b/client/apps/city_issues/templates/issues_list.html index 4877551..b615bf3 100644 --- a/client/apps/city_issues/templates/issues_list.html +++ b/client/apps/city_issues/templates/issues_list.html @@ -2,6 +2,7 @@ {% load i18n %} {% load static from staticfiles %} +{% load bootstrap3%} {% block meta_title %}All Issues{% endblock meta_title %} @@ -13,19 +14,19 @@

    All Issues:

    {{form.search}} - +
    - {% with search=request.GET.search order_by=request.GET.order_by reverse=request.GET.reverse %} + {% with order_by=request.GET.order_by reverse=request.GET.reverse %} - + From 72221ccfa5db97084c61fe8f4580a790fd73bf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9DAlex?= Date: Tue, 5 Dec 2017 18:32:50 +0200 Subject: [PATCH 185/385] add basic html template --- .../city_issues/templates/issue_detailed.html | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 client/apps/city_issues/templates/issue_detailed.html diff --git a/client/apps/city_issues/templates/issue_detailed.html b/client/apps/city_issues/templates/issue_detailed.html new file mode 100644 index 0000000..91399bb --- /dev/null +++ b/client/apps/city_issues/templates/issue_detailed.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% load i18n %} +{% load static from staticfiles %} + +{% block meta_title %}Detailed Issue{% endblock meta_title %} + +{% block content %} + +
    +
    + +
    +
    +
    +
      +
    • Category: {{ object.category}}
    • +
    • Latitude: {{ object.location_lat}}
    • +
    • Longitude: {{ object.location_lon}}
    • +
    • Status: {{ object.status}}
    • +
    • Description: {{ object.description}}
    • +
    • Open date: {{ object.open_date}}
    • +
    • Close date: {{ object.close_date}}
    • +
    • Delete date: {{ object.delete_date}}
    • +
    +
    +
    +
    + +{% endblock content %} \ No newline at end of file From f029483da0a545e0ee63889994e1124725d47ff0 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 5 Dec 2017 20:55:32 +0200 Subject: [PATCH 186/385] added user and role check on issue edit on map --- .../apps/city_issues/templates/map_page.html | 20 ++++----- client/apps/city_issues/views.py | 45 +++++++++---------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index b391c5c..8072bae 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -13,28 +13,26 @@ {% endblock extra_css %} {% block content %} - +

    {% csrf_token %} - {% for field in form %} -
    - {{field.errors}} - {{field.label_tag}} - {{field}} -
    - {% endfor %} + {% for field in form %} +
    + {{field.errors}} + {{field.label_tag}} + {{field}} +
    + {% endfor %}
    -
    - -
    +

    diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index 17fa417..ef4b976 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -9,9 +9,10 @@ from django.db.models import Q from django.views.generic.base import TemplateView, View +from django.conf import settings from django.contrib import messages from django.core import serializers -from django.http import JsonResponse, HttpResponse +from django.http import JsonResponse, HttpResponse, Http404 from django.shortcuts import redirect, render from django.urls import reverse from django.utils.timezone import make_aware @@ -21,7 +22,9 @@ from city_issues.models import Attachments, Issues, IssueHistory, User from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, IssueFormEdit -DIR_WITH_IMGS = "media" +ROLE_ADMIN = 1 +ROLE_MODERATOR = 2 +ROLE_USER = 3 class HomePageView(TemplateView): @@ -94,17 +97,12 @@ def get_issue_data(request, issue_id): Attachments.objects.filter(issue=issue_id).values()) images_urls = [item['image_url'] for item in attachments_query] checked_img_urls = [] - for img in images_urls: - - full_img_url = "/".join( - [os.path.dirname(os.path.abspath(__file__)), - DIR_WITH_IMGS, - str(img)]) - if not os.path.isfile(str(full_img_url)): - checked_img_urls.append(None) - else: + for img in images_urls: + if img and os.path.isfile(os.path.join(settings.MEDIA_ROOT, img)): checked_img_urls.append(img) + else: + checked_img_urls.append(None) issue_query = list(Issues.objects.filter(pk=issue_id).values()) issue_dict = issue_query[0] @@ -128,24 +126,19 @@ def get_all_issues_data(request): if form.is_valid() and form.data.get('filter'): - map_date_from = form.data.get('date_from') - map_date_to = form.data.get('date_to') - show_closed = form.data.get('show_closed') + map_date_from = form.cleaned_data.get('date_from') + map_date_to = form.cleaned_data.get('date_to') + show_closed = form.cleaned_data.get('show_closed') category = form.data.get('category') - search = form.data.get('search') + search = form.cleaned_data.get('search') - if show_closed == 'true': - show_closed = False - else: - show_closed = True + show_closed = not show_closed kwargs = {"close_date__isnull": (show_closed)} if map_date_from and map_date_to: - date_from = make_aware(datetime.combine( - datetime.strptime(map_date_from, '%Y-%m-%d'), time.min)) - date_to = make_aware(datetime.combine( - datetime.strptime(map_date_to, '%Y-%m-%d'), time.max)) + date_from = make_aware(datetime.combine(map_date_from, time.min)) + date_to = make_aware(datetime.combine(map_date_to, time.max)) kwargs["open_date__range"] = (date_from, date_to) if category: @@ -198,3 +191,9 @@ class UpdateIssue(UpdateView): form_class = IssueFormEdit template_name = 'edit_issue.html' success_url = '/map/' + + def dispatch(self, request, *args, **kwargs): + obj = self.get_object() + if (self.request.user.role.id in (ROLE_ADMIN, ROLE_MODERATOR)) or (obj.user == self.request.user): + return super(UpdateIssue, self).dispatch(request, *args, **kwargs) + raise Http404("You are not allowed to edit this issue") From 032f62ca0965765ce2dd66dcaa0916889d4b7be2 Mon Sep 17 00:00:00 2001 From: Andrii Sereda Date: Tue, 5 Dec 2017 23:16:00 +0200 Subject: [PATCH 187/385] save password --- backend/insert_db_data.py | 1 - backend/views/views.py | 1 - client/apps/city_issues/forms/forms.py | 29 ++++++------ client/apps/city_issues/models/users.py | 5 +++ .../city_issues/templates/issues/issues.html | 2 +- client/apps/city_issues/views.py | 44 ++++++++++++++----- 6 files changed, 54 insertions(+), 28 deletions(-) diff --git a/backend/insert_db_data.py b/backend/insert_db_data.py index 616b817..71d9941 100644 --- a/backend/insert_db_data.py +++ b/backend/insert_db_data.py @@ -33,7 +33,6 @@ user3 = User(name='Maria', alias='Mary', email='maria@gmail.com', password='321', role_id='3') - issue1 = Issue(title='Road accident', user_id='2', category_id='1', location_lat='50.621945', location_lon='26.249314', description='Car crash ...', status='on moderation', open_date='2017/11/15') diff --git a/backend/views/views.py b/backend/views/views.py index 75bd9f7..1c8ea4a 100644 --- a/backend/views/views.py +++ b/backend/views/views.py @@ -247,7 +247,6 @@ def issue_modify(issue_id): return render_template('issue_modify.html', form=form, route_to=route_to) - @app.route('/deleteissue/', methods=['POST']) @admin_permissions def delete_issue(issue_id): diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index 00394fe..1549658 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -96,20 +96,16 @@ class EditUserForm(forms.ModelForm): class Meta: model = User fields = ['name', 'alias', 'email'] - - def clean_email(self): - # Get the email - email = self.cleaned_data.get('email') - - # Check to see if any users already exist with this email as a username. - try: - match = User.objects.get(email=email) - except User.DoesNotExist: - # Unable to find a user, this is fine - return email - - # A user was found with this as a username, raise an error. - raise forms.ValidationError('This email address is already in use.') + # + # def clean_email(self): + # alias = self.cleaned_data["alias"] + # email = self.cleaned_data["email"] + # users = User.objects.filter(email__iexact=email).exclude( + # alias__iexact=alias) + # if users: + # raise forms.ValidationError( + # 'User with that email already exists.') + # return email.lower() name = forms.CharField( max_length=25, @@ -130,18 +126,21 @@ def clean_email(self): ) current_password = forms.CharField( + required=False, + min_length=4, widget=forms.PasswordInput(attrs={'class': 'form-control'}) ) new_password = forms.CharField( + required=False, max_length=50, min_length=4, widget=forms.PasswordInput(attrs={'class': 'form-control'}) ) confirm_password = forms.CharField( + required=False, max_length=50, min_length=4, widget=forms.PasswordInput(attrs={'class': 'form-control'}) ) - diff --git a/client/apps/city_issues/models/users.py b/client/apps/city_issues/models/users.py index c94b32f..8f6e15b 100644 --- a/client/apps/city_issues/models/users.py +++ b/client/apps/city_issues/models/users.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.hashers import make_password from django.db import models +from passlib.handlers.django import django_bcrypt from city_issues.user_managers import UserManager @@ -85,6 +86,10 @@ def set_password(self, raw_password): self.hashed_password = make_password(raw_password) self._password = raw_password + def check_password(self, raw_password): + """Checking the password form database.""" + return django_bcrypt.verify(raw_password, self.hashed_password) + @property def is_active(self): """...""" diff --git a/client/apps/city_issues/templates/issues/issues.html b/client/apps/city_issues/templates/issues/issues.html index 62ccae3..bd2fc70 100644 --- a/client/apps/city_issues/templates/issues/issues.html +++ b/client/apps/city_issues/templates/issues/issues.html @@ -73,7 +73,7 @@

    Add new issue

    - {{ form.file }} + {{ form.files }}
    diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index e1d909f..107a0a2 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -40,20 +40,44 @@ def get(self, request): 'form': form}) def post(self, request): - if request.method == 'POST': - form = EditUserForm(request.POST, instance=request.user) - if form.is_valid(): - print form.cleaned_data - self.check_passwords(form.cleaned_data['current_password']) + user = User.objects.get(id=request.user.id) + form = EditUserForm(data=request.POST, instance=request.user) - else: - messages.error(request, form.errors) + if form.is_valid(): + print user.id + print user.name + if self.is_not_empty_passwrds(form.cleaned_data) and self.check_passwords(user, form.cleaned_data): + user.set_password(form.cleaned_data['confirm_password']) + + user.name = form.cleaned_data['name'] + user.alias = form.cleaned_data['alias'] + user.email = form.cleaned_data['email'] + user.save() + messages.success(request, 'Changes successfully saved') + + else: + messages.error(request, form.errors) return redirect(self.success_url) - def check_passwords(self, current_pass): - if not django_bcrypt.verify(current_pass, self.request.user.password): - return messages.error(self.request, 'Wrong password') + def check_passwords(self, user, form_data): + print user.id + if not user.check_password(form_data['current_password']): + messages.error(self.request, 'Wrong current password') + return redirect(self.success_url) + + elif not form_data['new_password'] == form_data['confirm_password']: + messages.error(self.request, "New and confirm password don't match") + return redirect(self.success_url) + + else: + return True + + def is_not_empty_passwrds(self, data): + if data['current_password'] == '' and data['new_password'] == '' and data['confirm_password'] == '': + return False + else: + return True class IssueCreate(CreateView): From 11dec854b3a8e510d285dfad6bd7db53172963c5 Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Wed, 6 Dec 2017 02:26:38 +0200 Subject: [PATCH 188/385] modified search, deleted docstring in class Meta --- backend/forms/forms.py | 3 -- client/apps/city_issues/forms/forms.py | 18 +++++++++-- client/apps/city_issues/models/__init__.py | 1 - client/apps/city_issues/models/issues.py | 5 ---- client/apps/city_issues/models/users.py | 6 ---- .../city_issues/static/city_issues/js/main.js | 23 ++++++++++++++ .../city_issues/templates/issues_list.html | 30 +++++++++++-------- client/apps/city_issues/views.py | 26 ++-------------- client/basic/urls.py | 4 +-- 9 files changed, 60 insertions(+), 56 deletions(-) diff --git a/backend/forms/forms.py b/backend/forms/forms.py index 78dff3e..9d0329d 100644 --- a/backend/forms/forms.py +++ b/backend/forms/forms.py @@ -55,7 +55,6 @@ def __call__(self, form, field): class BaseForm(FlaskForm): """Adds csrf""" class Meta: - """...""" csrf = True @@ -251,7 +250,6 @@ class SearchUserForm(BaseForm): ) class Meta: - """...""" csrf = False @@ -277,5 +275,4 @@ class SearchIssuesForm(BaseForm): ) class Meta: - """...""" csrf = False diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index b9d63bc..0e49abd 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -105,12 +105,26 @@ class IssueFilter(forms.Form): }), required=False,) + class IssueSearchForm(forms.Form): "Issue search form" search = forms.CharField( min_length=2, max_length=100, + label='', widget=forms.TextInput(attrs={ - 'size':'60%', + 'size': '60%' }), - required=False,) + required=False) + order_by = forms.CharField( + max_length=15, + widget=forms.HiddenInput(), + required=False) + reverse = forms.CharField( + max_length=3, + widget=forms.HiddenInput(), + required=False) + page = forms.IntegerField( + widget=forms.HiddenInput(), + required=False) + diff --git a/client/apps/city_issues/models/__init__.py b/client/apps/city_issues/models/__init__.py index 51b197a..c982ed1 100644 --- a/client/apps/city_issues/models/__init__.py +++ b/client/apps/city_issues/models/__init__.py @@ -1,3 +1,2 @@ -"""...""" from .users import User from .issues import Issues, Attachments, Category, IssueHistory, Statuses diff --git a/client/apps/city_issues/models/issues.py b/client/apps/city_issues/models/issues.py index e5576e3..f8d33c7 100644 --- a/client/apps/city_issues/models/issues.py +++ b/client/apps/city_issues/models/issues.py @@ -24,7 +24,6 @@ def get_file_path(self, filename): image_url = models.ImageField(blank=True, null=True, upload_to=get_file_path) class Meta: - """...""" app_label = 'city_issues' managed = False db_table = 'attachments' @@ -41,7 +40,6 @@ def __unicode__(self): return u'{0}'.format(self.category) class Meta: - """...""" app_label = 'city_issues' managed = False db_table = 'category' @@ -54,7 +52,6 @@ class Statuses(models.Model): status = models.TextField(blank=True, null=True) class Meta: - """...""" app_label = 'city_issues' managed = False db_table = 'statuses' @@ -76,7 +73,6 @@ class IssueHistory(models.Model): auto_now_add=True) class Meta: - """...""" app_label = 'city_issues' managed = False db_table = 'issue_History' @@ -99,7 +95,6 @@ class Issues(models.Model): delete_date = models.DateTimeField(blank=True, null=True) class Meta: - """...""" app_label = 'city_issues' managed = False db_table = 'issues' diff --git a/client/apps/city_issues/models/users.py b/client/apps/city_issues/models/users.py index c94b32f..12d7ed6 100644 --- a/client/apps/city_issues/models/users.py +++ b/client/apps/city_issues/models/users.py @@ -20,7 +20,6 @@ class Role(models.Model): app_label = 'city_issues' class Meta: - """...""" managed = False db_table = 'roles' @@ -74,7 +73,6 @@ def get_short_name(self): @property def password(self): - """...""" return self.hashed_password @password.setter @@ -87,7 +85,6 @@ def set_password(self, raw_password): @property def is_active(self): - """...""" return not self.delete_date @is_active.setter @@ -96,7 +93,6 @@ def is_active(self, value): @property def is_staff(self): - """...""" return self.role == ROLE_MODERATOR @is_staff.setter @@ -108,7 +104,6 @@ def is_staff(self, value): @property def is_superuser(self): - """...""" return self.role == ROLE_ADMIN @is_superuser.setter @@ -119,6 +114,5 @@ def is_superuser(self, value): self.role = ROLE_USER class Meta: - """...""" managed = False db_table = 'users' diff --git a/client/apps/city_issues/static/city_issues/js/main.js b/client/apps/city_issues/static/city_issues/js/main.js index 70b2248..d70e233 100644 --- a/client/apps/city_issues/static/city_issues/js/main.js +++ b/client/apps/city_issues/static/city_issues/js/main.js @@ -196,6 +196,29 @@ function insertTemplate(parentId, templateId) { } } +$( document ).ready(function() { + $('.get-request').on('click', function(e) { + e.preventDefault(); + + var search = $('#search-filter').data('search'); + var order_by = $(this).data('order-by'); + var reverse = $(this).data('reverse'); + var page = $(this).data('page'); + + $("#id_search").val(search); + $("#id_order_by").val(order_by); + $("#id_reverse").val(reverse); + $("#id_page").val(page); + + $('#submit').click(); + }); + + $('#search-filter').on('click', function(e){ + if (order_by == ""){ + $("#id_order_by").val('title'); + } + }); +}); issueMap = new IssueMap("mapid"); diff --git a/client/apps/city_issues/templates/issues_list.html b/client/apps/city_issues/templates/issues_list.html index b615bf3..eb3f49e 100644 --- a/client/apps/city_issues/templates/issues_list.html +++ b/client/apps/city_issues/templates/issues_list.html @@ -10,13 +10,15 @@
    + {% with search=request.GET.search order_by=request.GET.order_by %} + {% endwith %}
    @@ -26,7 +28,7 @@

    All Issues:

    + - + - + +
    # - + Title {% if order_by == 'title' and reverse != 'v_v' %}↑ {% elif order_by != 'status' and order_by != 'description' and order_by != 'category' and reverse != 'v_v' %}↑ @@ -34,7 +35,7 @@

    All Issues:

    - + Status {% if order_by == 'status' and reverse != 'v_v' %}↑ {% elif order_by == 'status' and reverse == 'v_v' %}↓ @@ -42,7 +43,7 @@

    All Issues:

    - + Description {% if order_by == 'description' and reverse != 'v_v' %}↑ {% elif order_by == 'description' and reverse == 'v_v' %}↓ @@ -50,7 +51,7 @@

    All Issues:

    - + Category {% if order_by == 'category' and reverse != 'v_v' %}↑ {% elif order_by == 'category' and reverse == 'v_v' %}↓ diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index c753daf..fabe076 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -175,6 +175,15 @@ class CheckIssues(ListView, FormView): context_object_name = 'issues_list' paginate_by = 6 + # def search_issue(self): + # search = self.request.GET.get('search') + # all_issue = Issues.objects.all() + # if search: + # query_list = search.split() + # issues_list = all_issue.filter( + # reduce(operator.or_, (Q(title__icontains=q) for q in query_list)) | + # reduce(operator.or_, (Q(description__icontains=q) for q in query_list))) + # return issues_list def get_queryset(self): """Adds sorting""" @@ -188,7 +197,6 @@ def get_queryset(self): reduce(operator.or_, (Q(description__icontains=q) for q in query_list)) ) if order_by in ('title', 'status', 'description', 'category'): - print queryset queryset = queryset.order_by(order_by) if self.request.GET.get('reverse', '') == 'v_v': queryset = queryset.reverse() @@ -198,3 +206,15 @@ def get_context_data(self, **kwargs): context = super(CheckIssues, self).get_context_data(**kwargs) context['issues_range'] = range(context["paginator"].num_pages) return context + +def search_issue(request): + """ Search issues """ + form = IssueSearchForm + search = request.GET.get('search') + all_issue = Issues.objects.all() + if search: + query_list = search.split() + issues_list = all_issue.filter( + reduce(operator.or_, (Q(title__icontains=q) for q in query_list)) | + reduce(operator.or_, (Q(description__icontains=q) for q in query_list))) + return render (request, 'issues_list.html', {'form': form, 'issues_list': issues_list}) \ No newline at end of file diff --git a/client/basic/urls.py b/client/basic/urls.py index 60eadd0..193bd58 100644 --- a/client/basic/urls.py +++ b/client/basic/urls.py @@ -22,14 +22,14 @@ from city_issues.views import ( CheckIssues, edit_issue_view, get_all_issues_data, get_issue_data, - HomePageView, map_page_view, IssueCreate, UserProfileView) + HomePageView, map_page_view, IssueCreate, search_issue, UserProfileView) from city_issues.models import Issues urlpatterns = [ url(r'^$', HomePageView.as_view(), name='home'), - url(r'^issues/$', CheckIssues.as_view(queryset=Issues.objects.filter(status='new')), name='issues'), - + url(r'^issues/$', CheckIssues.as_view(), name='issues'), + url(r'^issues/search/$', search_issue, name='search'), url(r'^map/$', map_page_view, name='map'), url(r'^map/getissuebyid/(?P[0-9]+)$', From 6386eb8629600e80b48a3ecf901d810a8e5f6ce6 Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 4 Dec 2017 22:20:05 +0200 Subject: [PATCH 181/385] upgrade filter and marker drag --- .../static/city_issues/css/style.css | 5 ++- .../static/city_issues/js/edit_on_map.js | 26 +++++++++++-- .../city_issues/static/city_issues/js/main.js | 37 ++++++++++++++----- .../apps/city_issues/templates/map_page.html | 25 +++++++------ 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/client/apps/city_issues/static/city_issues/css/style.css b/client/apps/city_issues/static/city_issues/css/style.css index 37d2629..8840e2e 100644 --- a/client/apps/city_issues/static/city_issues/css/style.css +++ b/client/apps/city_issues/static/city_issues/css/style.css @@ -2,7 +2,6 @@ height: 900px; } - .issue_container { position: fixed; top: 0; @@ -61,4 +60,8 @@ font-size: 16px; color: red; +} + +.issue_form-container { + display: none; } \ No newline at end of file diff --git a/client/apps/city_issues/static/city_issues/js/edit_on_map.js b/client/apps/city_issues/static/city_issues/js/edit_on_map.js index da59abd..04da286 100644 --- a/client/apps/city_issues/static/city_issues/js/edit_on_map.js +++ b/client/apps/city_issues/static/city_issues/js/edit_on_map.js @@ -13,23 +13,41 @@ this.layer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(this.map); - this.marker = L.marker([this.lat, this.lng], { draggable: false}).addTo(this.map); + this.marker = L.marker([this.lat, this.lng], { draggable: true}).addTo(this.map); + this.map.on('click',($.proxy(this.getLocation, this))); - + this.marker.on('dragend',($.proxy(this.getDragLocation, this))); + } IssueMap.prototype.getLocation = function (e) { if(this.map.hasLayer(this.marker)) this.map.removeLayer(this.marker); var location = e.latlng; - this.marker = L.marker(location, { draggable: false}).addTo(this.map); + this.marker = L.marker(location, { draggable: true}).addTo(this.map); + this.marker.off('dragend'); + this.marker.on('dragend',($.proxy(this.getDragLocation, this))); + $.proxy(this.setInputLocation(location), this); + }; + +IssueMap.prototype.getDragLocation = function (e) { + if(this.map.hasLayer(this.marker)) + this.map.removeLayer(this.marker); + this.marker = e.target; + var location = this.marker.getLatLng(); + this.marker.setLatLng(location, { draggable: true}).addTo(this.map); + $.proxy(this.setInputLocation(location), this); + }; + + +IssueMap.prototype.setInputLocation = function(location) { $("input[name=location_lat]").val(location.lat); $("input[name=location_lon]").val(location.lng); localStorage.setItem('lat', location.lat); localStorage.setItem('lng', location.lng); - }; + }; $(function () { diff --git a/client/apps/city_issues/static/city_issues/js/main.js b/client/apps/city_issues/static/city_issues/js/main.js index 3e18c6f..153e873 100644 --- a/client/apps/city_issues/static/city_issues/js/main.js +++ b/client/apps/city_issues/static/city_issues/js/main.js @@ -4,8 +4,16 @@ function IssueMap(elementId) { var current = this; this.map = L.map(elementId); - IssueMap.prototype.setFilterFromBtn = function(FilterFromBtnId) { - current.filterFormBtn = document.querySelector(FilterFromBtnId); + IssueMap.prototype.setFilterFromBtn = function(filterFormBtnId) { + current.filterFormBtn = document.querySelector(filterFormBtnId); + }; + + IssueMap.prototype.setFilterFromCloseBtn = function(filterFormCloseBtnId) { + current.filterFormCloseBtnId = document.querySelector(filterFormCloseBtnId); + }; + + IssueMap.prototype.setFilterFromShowBtn = function(filterFormShowBtnId) { + current.filterFormShowBtnId = document.querySelector(filterFormShowBtnId); }; @@ -89,7 +97,7 @@ function IssueMap(elementId) { }; - IssueMap.prototype.lanlatHendler = function(event) { + IssueMap.prototype.mapCoordinatesHandler = function(event) { if (event.originalEvent.target.classList.contains("leaflet-marker-icon")) { localStorage.setItem('lat', event.latlng.lat); localStorage.setItem('lng', event.latlng.lng); @@ -97,9 +105,21 @@ function IssueMap(elementId) { }; + IssueMap.prototype.filterCloseHandler = function(event) { + document.querySelector("#issue_form-container").style.display = "none"; + current.filterFormShowBtnId.style.display = "block"; + }; + + IssueMap.prototype.filterShowHandler = function(event) { + document.querySelector("#issue_form-container").style.display = "block"; + current.filterFormShowBtnId.style.display = "none"; + }; + IssueMap.prototype.addHandler = function() { current.filterFormBtn.addEventListener('click', current.filterHandler); - current.map.on('click', current.lanlatHendler); + current.map.on('click', current.mapCoordinatesHandler); + current.filterFormCloseBtnId.addEventListener('click', current.filterCloseHandler); + current.filterFormShowBtnId.addEventListener('click', current.filterShowHandler); }; IssueMap.prototype.getMarkers = function(serverURL) { @@ -135,7 +155,6 @@ function IssueDescription(mapId, issueContainerId, issueCloseId) { if (event.target.id.slice(0,16) == "issue_primary-id") { current.getIssueById(event.target.id.slice(16)); current.issue_box.style.display = 'block'; - console.log(event); } else { if (event.target.id == "mapid" && current.issue_box.style.display == "block") { @@ -190,10 +209,10 @@ function IssueDescription(mapId, issueContainerId, issueCloseId) { } function bootstrapCarousel(images_urls) { - var media = "/media/"; $(document).ready(function(){ for(var i=0 ; i< images_urls.length ; i++) { - $('
    ').appendTo('.carousel-inner'); + var fullImgUrl = "/media/" + images_urls[i]; + $('
    ').appendTo('.carousel-inner'); $('
  • ').appendTo('.carousel-indicators'); } $('.item').first().addClass('active'); @@ -212,10 +231,10 @@ function insertTemplate(parentId, templateId) { } } - - issueMap = new IssueMap("mapid"); issueMap.setFilterFromBtn("#issue_filter-form-btn"); +issueMap.setFilterFromCloseBtn("#issue_filter-form-close-btn"); +issueMap.setFilterFromShowBtn("#issue_filter-form-show-btn"); issueMap.setViewPoint(50.621945, 26.249314, 16); issueMap.addMapLayer( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index ead3f0d..b391c5c 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -15,17 +15,20 @@ {% block content %}
    -

    - {% csrf_token %} - {% for field in form %} -
    - {{field.errors}} - {{field.label_tag}} - {{field}} -
    - {% endfor %} - - + +
    +

    + {% csrf_token %} + {% for field in form %} +
    + {{field.errors}} + {{field.label_tag}} + {{field}} +
    + {% endfor %} + + +
    From 0fc6ab74f3aac3b9d97c7616e2b8914abfd1330d Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 5 Dec 2017 10:39:18 +0200 Subject: [PATCH 182/385] fix not existing img bug on map --- .../city_issues/static/city_issues/js/main.js | 12 +++++++++--- client/apps/city_issues/views.py | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/client/apps/city_issues/static/city_issues/js/main.js b/client/apps/city_issues/static/city_issues/js/main.js index 153e873..34be366 100644 --- a/client/apps/city_issues/static/city_issues/js/main.js +++ b/client/apps/city_issues/static/city_issues/js/main.js @@ -211,9 +211,15 @@ function IssueDescription(mapId, issueContainerId, issueCloseId) { function bootstrapCarousel(images_urls) { $(document).ready(function(){ for(var i=0 ; i< images_urls.length ; i++) { - var fullImgUrl = "/media/" + images_urls[i]; - $('
    ').appendTo('.carousel-inner'); - $('
  • ').appendTo('.carousel-indicators'); + var fullImgUrl = images_urls[i]; + if (fullImgUrl !== null) { + fullImgUrl = "/media/" + fullImgUrl; + } else { + fullImgUrl = "/static/city_issues/img/no-image.png"; + } + + $('
    ').appendTo('.carousel-inner'); + $('
  • ').appendTo('.carousel-indicators'); } $('.item').first().addClass('active'); $('.carousel-indicators > li').first().addClass('active'); diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index 7cb8feb..17fa417 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -3,6 +3,8 @@ """ # -*- coding: utf-8 -*- import json +import os.path + from datetime import date, datetime, time from django.db.models import Q @@ -19,6 +21,8 @@ from city_issues.models import Attachments, Issues, IssueHistory, User from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, IssueFormEdit +DIR_WITH_IMGS = "media" + class HomePageView(TemplateView): """Home page""" @@ -89,10 +93,22 @@ def get_issue_data(request, issue_id): attachments_query = list( Attachments.objects.filter(issue=issue_id).values()) images_urls = [item['image_url'] for item in attachments_query] + checked_img_urls = [] + for img in images_urls: + + full_img_url = "/".join( + [os.path.dirname(os.path.abspath(__file__)), + DIR_WITH_IMGS, + str(img)]) + + if not os.path.isfile(str(full_img_url)): + checked_img_urls.append(None) + else: + checked_img_urls.append(img) issue_query = list(Issues.objects.filter(pk=issue_id).values()) issue_dict = issue_query[0] - issue_dict['images_urls'] = images_urls + issue_dict['images_urls'] = checked_img_urls issue_dict['open_date'] = convert_date(issue_dict['open_date']) issue_dict['close_date'] = convert_date(issue_dict['close_date']) From ef049e67de9120bbfcf21f1526f36a57a0db67e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9DAlex?= Date: Tue, 5 Dec 2017 17:08:10 +0200 Subject: [PATCH 183/385] add basic detailed page --- client/apps/city_issues/templates/base.html | 4 ++++ client/apps/city_issues/views.py | 7 +++++++ client/basic/urls.py | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/client/apps/city_issues/templates/base.html b/client/apps/city_issues/templates/base.html index 2015b4f..7a595d4 100644 --- a/client/apps/city_issues/templates/base.html +++ b/client/apps/city_issues/templates/base.html @@ -37,6 +37,8 @@
    Log out + All Issues {% else %} @@ -44,6 +46,8 @@ class="navbar-text navbar-right">Log in Sign in + All Issues {% endif %} diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index ac67861..8d863cb 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -14,6 +14,7 @@ from django.urls import reverse from django.utils.timezone import make_aware from django.views.generic import CreateView, ListView +from django.views.generic.detail import DetailView from city_issues.models import Attachments, Issues, IssueHistory, User from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm @@ -187,3 +188,9 @@ def get_context_data(self, **kwargs): context = super(CheckIssues, self).get_context_data(**kwargs) context['issues_range'] = range(context["paginator"].num_pages) return context + + +class DetailedIssue(DetailView): + """Detailed issue""" + template_name = 'issue_detailed.html' + model = Issues diff --git a/client/basic/urls.py b/client/basic/urls.py index 678df8b..4dd3e6c 100644 --- a/client/basic/urls.py +++ b/client/basic/urls.py @@ -21,13 +21,14 @@ from django.views.generic import RedirectView from city_issues.views import ( - CheckIssues, edit_issue_view, get_all_issues_data, get_issue_data, + CheckIssues, DetailedIssue, edit_issue_view, get_all_issues_data, get_issue_data, HomePageView, map_page_view, IssueCreate, UserProfileView) urlpatterns = [ url(r'^$', HomePageView.as_view(), name='home'), url(r'^issues/$', CheckIssues.as_view(), name='issues'), + url(r'^issue/(?P\d+)/$', DetailedIssue.as_view(), name='issue'), url(r'^map/$', map_page_view, name='map'), From 562b0465aac48010799deefb6f2e964b0d2f0076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9DAlex?= Date: Tue, 5 Dec 2017 18:29:52 +0200 Subject: [PATCH 184/385] add links and connect pages --- client/apps/city_issues/templates/issues_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/apps/city_issues/templates/issues_list.html b/client/apps/city_issues/templates/issues_list.html index 6a00b2d..d6a750a 100644 --- a/client/apps/city_issues/templates/issues_list.html +++ b/client/apps/city_issues/templates/issues_list.html @@ -60,7 +60,7 @@

    All Issues:

    {% for issue in issues_list %}
    {{ forloop.counter }}{{ issue.title }}{{ issue.title }} {{ issue.status }} {{ issue.description }} {{ issue.category }}
    # - + Title {% if order_by == 'title' and reverse != 'v_v' %}↑ {% elif order_by != 'status' and order_by != 'description' and order_by != 'category' and reverse != 'v_v' %}↑ @@ -35,7 +37,7 @@

    All Issues:

    - + Status {% if order_by == 'status' and reverse != 'v_v' %}↑ {% elif order_by == 'status' and reverse == 'v_v' %}↓ @@ -43,7 +45,7 @@

    All Issues:

    - + Description {% if order_by == 'description' and reverse != 'v_v' %}↑ {% elif order_by == 'description' and reverse == 'v_v' %}↓ @@ -51,7 +53,7 @@

    All Issues:

    - + Category {% if order_by == 'category' and reverse != 'v_v' %}↑ {% elif order_by == 'category' and reverse == 'v_v' %}↓ @@ -79,12 +81,11 @@

    All Issues:

    {% if is_paginated %} -{% with order_by=request.GET.order_by reverse=request.GET.reverse %}
    -{% endwith %} {% endif %} {% endblock content %} + +{% block extra_js %} + +{% load static %} + +{% endblock extra_js %} diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index fabe076..3082cb0 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -16,7 +16,7 @@ from django.utils.timezone import make_aware from django.views.generic import CreateView, FormView, ListView -from city_issues.models import Attachments, Issues, IssueHistory, User +from city_issues.models import Attachments, Issues, IssueHistory, User from city_issues.forms.forms import EditIssue, IssueFilter, IssueForm, IssueSearchForm @@ -175,20 +175,10 @@ class CheckIssues(ListView, FormView): context_object_name = 'issues_list' paginate_by = 6 - # def search_issue(self): - # search = self.request.GET.get('search') - # all_issue = Issues.objects.all() - # if search: - # query_list = search.split() - # issues_list = all_issue.filter( - # reduce(operator.or_, (Q(title__icontains=q) for q in query_list)) | - # reduce(operator.or_, (Q(description__icontains=q) for q in query_list))) - # return issues_list - def get_queryset(self): """Adds sorting""" queryset = super(CheckIssues, self).get_queryset() - order_by = self.request.GET.get('order_by', 'title') + order_by = self.request.GET.get('order_by') search = self.request.GET.get('search') if search: query_list = search.split() @@ -206,15 +196,3 @@ def get_context_data(self, **kwargs): context = super(CheckIssues, self).get_context_data(**kwargs) context['issues_range'] = range(context["paginator"].num_pages) return context - -def search_issue(request): - """ Search issues """ - form = IssueSearchForm - search = request.GET.get('search') - all_issue = Issues.objects.all() - if search: - query_list = search.split() - issues_list = all_issue.filter( - reduce(operator.or_, (Q(title__icontains=q) for q in query_list)) | - reduce(operator.or_, (Q(description__icontains=q) for q in query_list))) - return render (request, 'issues_list.html', {'form': form, 'issues_list': issues_list}) \ No newline at end of file diff --git a/client/basic/urls.py b/client/basic/urls.py index 193bd58..aa065ce 100644 --- a/client/basic/urls.py +++ b/client/basic/urls.py @@ -22,14 +22,12 @@ from city_issues.views import ( CheckIssues, edit_issue_view, get_all_issues_data, get_issue_data, - HomePageView, map_page_view, IssueCreate, search_issue, UserProfileView) -from city_issues.models import Issues + HomePageView, map_page_view, IssueCreate, UserProfileView) urlpatterns = [ url(r'^$', HomePageView.as_view(), name='home'), url(r'^issues/$', CheckIssues.as_view(), name='issues'), - url(r'^issues/search/$', search_issue, name='search'), url(r'^map/$', map_page_view, name='map'), url(r'^map/getissuebyid/(?P[0-9]+)$', From 1cd85486b9a16ad3f504554812ad401cb8103025 Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Wed, 6 Dec 2017 08:59:56 +0200 Subject: [PATCH 189/385] fixed search --- client/apps/city_issues/forms/forms.py | 1 - .../city_issues/static/city_issues/js/main.js | 15 +++++++++++---- .../apps/city_issues/templates/issues_list.html | 4 ++-- client/apps/city_issues/views.py | 4 +++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index 0e49abd..68ff98b 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -127,4 +127,3 @@ class IssueSearchForm(forms.Form): page = forms.IntegerField( widget=forms.HiddenInput(), required=False) - diff --git a/client/apps/city_issues/static/city_issues/js/main.js b/client/apps/city_issues/static/city_issues/js/main.js index d70e233..89048c7 100644 --- a/client/apps/city_issues/static/city_issues/js/main.js +++ b/client/apps/city_issues/static/city_issues/js/main.js @@ -205,18 +205,25 @@ $( document ).ready(function() { var reverse = $(this).data('reverse'); var page = $(this).data('page'); + if (order_by == undefined){ + order_by = $('#search-filter').data('order'); + } + if (reverse == undefined){ + reverse = $('#search-filter').data('reverse'); + } + $("#id_search").val(search); $("#id_order_by").val(order_by); $("#id_reverse").val(reverse); $("#id_page").val(page); + $('#search-filter').data('order',order_by); $('#submit').click(); }); - $('#search-filter').on('click', function(e){ - if (order_by == ""){ - $("#id_order_by").val('title'); - } + $('#search-filter').on('submit', function(e){ + order_by = $('#search-filter').data('order'); + $("#id_order_by").val(order_by); }); }); diff --git a/client/apps/city_issues/templates/issues_list.html b/client/apps/city_issues/templates/issues_list.html index eb3f49e..309cb44 100644 --- a/client/apps/city_issues/templates/issues_list.html +++ b/client/apps/city_issues/templates/issues_list.html @@ -10,10 +10,10 @@
    - {% with search=request.GET.search order_by=request.GET.order_by %} + {% with search=request.GET.search order_by=request.GET.order_by reverse=request.GET.reverse %}
    -
    -
      -
    • Category: {{ object.category}}
    • -
    • Latitude: {{ object.location_lat}}
    • -
    • Longitude: {{ object.location_lon}}
    • -
    • Status: {{ object.status}}
    • -
    • Description: {{ object.description}}
    • -
    • Open date: {{ object.open_date}}
    • -
    • Close date: {{ object.close_date}}
    • -
    • Delete date: {{ object.delete_date}}
    • -
    -
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    CategoryLatitudeLongitudeStatusDescriptionOpen dateClose dateDelete date
    {{ object.category}}{{ object.location_lat}}{{ object.location_lon}}{{ issue.status }}{{ object.description}}{{ object.open_date}}{{ object.close_date}}{{ object.delete_date}}
    +
    From 64e06ac1b3d10e00b8431ec9635b95ded5e8e3ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9DAlex?= Date: Wed, 6 Dec 2017 18:03:31 +0200 Subject: [PATCH 192/385] add empty attachments field --- client/apps/city_issues/templates/issue_detailed.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/apps/city_issues/templates/issue_detailed.html b/client/apps/city_issues/templates/issue_detailed.html index 8bd293e..f01c82e 100644 --- a/client/apps/city_issues/templates/issue_detailed.html +++ b/client/apps/city_issues/templates/issue_detailed.html @@ -26,18 +26,20 @@

    {{ object.title }} {{ object.user }}

    Open date Close date Delete dateAttachments
    {{ object.category}}{{ object.category}} {{ object.location_lat}} {{ object.location_lon}}{{ issue.status }}{{ object.status }} {{ object.description}} {{ object.open_date}} {{ object.close_date}} {{ object.delete_date}}None
    From 5701acabba1d0445afb4e16972179fdb51212cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9DAlex?= Date: Wed, 6 Dec 2017 18:11:51 +0200 Subject: [PATCH 193/385] delete a useless field --- client/apps/city_issues/templates/issue_detailed.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/apps/city_issues/templates/issue_detailed.html b/client/apps/city_issues/templates/issue_detailed.html index f01c82e..de94a07 100644 --- a/client/apps/city_issues/templates/issue_detailed.html +++ b/client/apps/city_issues/templates/issue_detailed.html @@ -25,7 +25,6 @@

    {{ object.title }} {{ object.user }}

    Description Open date Close dateDelete date Attachments
    {{ object.description}} {{ object.open_date}} {{ object.close_date}}{{ object.delete_date}} None
    - + - + diff --git a/client/apps/city_issues/templates/issue_detailed.html b/client/apps/city_issues/templates/issue_detailed.html index 20fa313..b91b1af 100644 --- a/client/apps/city_issues/templates/issue_detailed.html +++ b/client/apps/city_issues/templates/issue_detailed.html @@ -22,7 +22,7 @@

    {{ object.title }} {{ object.user }}

    - + diff --git a/client/apps/city_issues/templates/issues_list.html b/client/apps/city_issues/templates/issues_list.html index 0a822a2..2dcca5d 100644 --- a/client/apps/city_issues/templates/issues_list.html +++ b/client/apps/city_issues/templates/issues_list.html @@ -44,7 +44,7 @@

    All Issues:

    {% endif %} -
    TitleCategotyCategory StatusDescriptionDescription Open Date
    Latitude Longitude StatusDescriptionDescription Open date Close date Attachments + Description {% if order_by == 'description' and reverse != 'v_v' %}↑ diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index a92bc60..1b7cd0f 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -9,7 +9,6 @@ import operator from django.db.models import Q -from django.views.generic.base import TemplateView, View from django.conf import settings from django.contrib import messages from django.core import serializers @@ -18,6 +17,7 @@ from django.urls import reverse from django.utils.timezone import make_aware from django.views.generic import CreateView, FormView, ListView +from django.views.generic.base import TemplateView, View from django.views.generic.detail import DetailView from django.views.generic.edit import UpdateView @@ -169,7 +169,7 @@ class CheckIssues(ListView, FormView): template_name = 'issues_list.html' model = Issues context_object_name = 'issues_list' - paginate_by = 6 + paginate_by = 4 def get_queryset(self): """Adds sorting""" From 8f895b9f97a320ab099a7824c57ec762b9c94113 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 7 Dec 2017 13:45:56 +0200 Subject: [PATCH 204/385] issue no-img bugfix --- client/apps/city_issues/static/city_issues/css/style.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/apps/city_issues/static/city_issues/css/style.css b/client/apps/city_issues/static/city_issues/css/style.css index ab924e3..1ac0575 100644 --- a/client/apps/city_issues/static/city_issues/css/style.css +++ b/client/apps/city_issues/static/city_issues/css/style.css @@ -23,6 +23,11 @@ height: 225px; } +.issue_img-box > .issue_img { + width: 100%; + height: 100%; +} + #carousel-example-generic img { height: 100%; width: 100%; From 37764fd1322a19e7f3469ad61d0c9823934723a6 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 7 Dec 2017 14:50:37 +0200 Subject: [PATCH 205/385] small UI fix --- client/apps/city_issues/templates/map_page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index 10cec3c..fc65236 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -15,7 +15,7 @@ {% block content %}
    - +

    {% csrf_token %} From 44c5d3d33753d38995a7c740d8818728dbbbc09c Mon Sep 17 00:00:00 2001 From: KostSvitlana Date: Thu, 7 Dec 2017 15:39:41 +0200 Subject: [PATCH 206/385] add functions in models --- backend/models/issues.py | 17 +++++++++++++++ backend/models/users.py | 26 ++++++++++++++++++++++ backend/templates/issue.html | 6 ------ backend/views/views.py | 42 ++++++------------------------------ 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/backend/models/issues.py b/backend/models/issues.py index 724b3b5..f9a6c72 100644 --- a/backend/models/issues.py +++ b/backend/models/issues.py @@ -110,3 +110,20 @@ class Meta: app_label = 'city_issues' managed = False db_table = 'comments' + + +def get_all_issue_history(issue_id): + """Method return all issue history and comments sorted by date.""" + history = db.session.query( + IssueHistory).filter(IssueHistory.issue_id == issue_id).order_by( + IssueHistory.transaction_date).all() + comments = db.session.query(Comments).filter( + Comments.issue_id == issue_id).order_by(Comments.date_public).all() + list_history = [] + for histor in history: + list_history.append(['change_status', histor.status.status, histor.user.alias, histor.transaction_date.strftime('%Y-%m-%d %H:%M')]) + for comment in comments: + list_history.append(['add_comment', comment.user.alias, comment.comment, comment.date_public.strftime('%Y-%m-%d %H:%M')]) + list_history.sort(key=lambda history: history[3]) + return list_history + diff --git a/backend/models/users.py b/backend/models/users.py index 20088ae..f9613dd 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -1,6 +1,7 @@ """This module creates Users model.""" # pylint: disable=too-few-public-methods +from sqlalchemy import or_ from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.sql.functions import func from passlib.hash import django_bcrypt @@ -78,3 +79,28 @@ def restore(self): self.delete_date = None return True return False + + +def user_search(search_string, search_by): + """Method user search.""" + MIN_SEARCH_STR = 2 + + condition_list = [] + for one_string in search_string.split(): + if len(one_string) < MIN_SEARCH_STR: + continue + search_parameter = '%{}%'.format(one_string) + name_search = User.name.ilike(search_parameter) + alias_search = User.alias.ilike(search_parameter) + email_search = User.email.ilike(search_parameter) + conditions = [ + name_search, + alias_search, + email_search, + or_(name_search, alias_search), + or_(alias_search, email_search), + or_(email_search, name_search), + or_(name_search, alias_search, email_search) + ] + condition_list.append(conditions[search_by]) + return or_(*condition_list) diff --git a/backend/templates/issue.html b/backend/templates/issue.html index e116985..c98a499 100644 --- a/backend/templates/issue.html +++ b/backend/templates/issue.html @@ -37,12 +37,6 @@
    {{issue.location_lat}}, {{issue.location_lon }}
    Last status:
    {{ issue.status }}
    - {% if attachments %} -
    Attachments:
    - {% for attachment in attachments %} -
    {{ attachment.image_url}}
    - {% endfor %} - {% endif %}
    diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index 5979f32..dc51c16 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -35,6 +35,9 @@ class HomePageView(TemplateView): """Home page""" template_name = "home_page.html" + def get(self, request): + return redirect("map") + class UserProfileView(View): """User profile page""" From 2ba9b3f833e6e70ec4eaf903f0dbd03b7c509089 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 7 Dec 2017 21:31:16 +0200 Subject: [PATCH 213/385] map template fix --- .../apps/city_issues/templates/map_page.html | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index 443a075..c1c0cb9 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -14,24 +14,8 @@ {% endblock extra_css %} {% block content %} - -
    - -
    -

    - {% csrf_token %} - {% for field in form %} -
    - {{field.errors}} - {{field.label_tag}} - {{field}} -
    - {% endfor %} - - -
    -
    -
    + +
    {% if messages %} @@ -48,18 +32,26 @@
    -
    -
    -
    - {% csrf_token %} - {{ form }} - -
    + +
    + +
    +

    + {% csrf_token %} + {% for field in form %} +
    + {{field.errors}} + {{field.label_tag}} + {{field}} +
    + {% endfor %} + +
    -
    -
    +
    +

    From c331738e95dfaba616344e86fe97bd1618694af5 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 7 Dec 2017 22:02:54 +0200 Subject: [PATCH 214/385] hide edit for user without permissions --- .../city_issues/static/city_issues/js/main.js | 9 +++++++-- .../city_issues/templates/edit_issue.html | 4 +++- client/apps/city_issues/views.py | 19 ++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/client/apps/city_issues/static/city_issues/js/main.js b/client/apps/city_issues/static/city_issues/js/main.js index 988c8bb..dea0763 100644 --- a/client/apps/city_issues/static/city_issues/js/main.js +++ b/client/apps/city_issues/static/city_issues/js/main.js @@ -190,8 +190,13 @@ function IssueDescription(mapId, issueContainerId, issueCloseId) { img.classList.add("issue_img"); imgBox.appendChild(img); } - var dataUrl = document.getElementById("issue_edit").getAttribute("data-url").slice(0,-1); - document.getElementById("issue_edit").setAttribute("href", dataUrl + jsonData.id); + var editBtn = document.getElementById("issue_edit"); + var dataUrl = editBtn.getAttribute("data-url").slice(0,-1); + editBtn.setAttribute("href", dataUrl + jsonData.id); + editBtn.style.display = "none"; + if (jsonData.editable) { + editBtn.style.display = "inline-block"; + } }; diff --git a/client/apps/city_issues/templates/edit_issue.html b/client/apps/city_issues/templates/edit_issue.html index 8848f9b..39a07a1 100644 --- a/client/apps/city_issues/templates/edit_issue.html +++ b/client/apps/city_issues/templates/edit_issue.html @@ -16,8 +16,10 @@
    {% csrf_token %} {{ form.as_p }} - +
    +
    + Return to map
    diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index f9a3683..c6a5f6d 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -83,7 +83,8 @@ def check_passwords(self, user, form_data): return redirect(self.success_url) elif not form_data['new_password'] == form_data['confirm_password']: - messages.error(self.request, "New and confirm password don't match") + messages.error( + self.request, "New and confirm password don't match") return redirect(self.success_url) else: @@ -156,9 +157,20 @@ def get_issue_data(request, issue_id): if img and os.path.isfile(os.path.join(settings.MEDIA_ROOT, img)): checked_img_urls.append(img) - issue_query = list(Issues.objects.filter(pk=issue_id).values()) + issue_obj = Issues.objects.filter(pk=issue_id) + unpacked_issue_obj = issue_obj[0] + + issue_is_editable = False + + if hasattr(request.user, 'role') and ( + (request.user.role.id in (ROLE_ADMIN, ROLE_MODERATOR)) or + (unpacked_issue_obj.user_id == request.user.id)): + issue_is_editable = True + + issue_query = list(issue_obj.values()) issue_dict = issue_query[0] issue_dict['images_urls'] = checked_img_urls + issue_dict['editable'] = issue_is_editable issue_dict['open_date'] = convert_date(issue_dict['open_date']) issue_dict['close_date'] = convert_date(issue_dict['close_date']) @@ -231,7 +243,8 @@ def get_queryset(self): query_list = search.split() queryset = queryset.filter( reduce(operator.or_, (Q(title__icontains=q) for q in query_list)) | - reduce(operator.or_, (Q(description__icontains=q) for q in query_list)) + reduce(operator.or_, (Q(description__icontains=q) + for q in query_list)) ) if order_by in ('title', 'status', 'description', 'category'): queryset = queryset.order_by(order_by) From 631fef512ef702be4c40044ea12f28df7cc75e5e Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 7 Dec 2017 22:14:49 +0200 Subject: [PATCH 215/385] map css fix --- client/apps/city_issues/static/city_issues/css/style.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/apps/city_issues/static/city_issues/css/style.css b/client/apps/city_issues/static/city_issues/css/style.css index e653dc3..d8dd866 100644 --- a/client/apps/city_issues/static/city_issues/css/style.css +++ b/client/apps/city_issues/static/city_issues/css/style.css @@ -23,6 +23,11 @@ height: 225px; } +.issue_img-box > .issue_img { + width: 100%; + height: 100%; +} + #carousel-example-generic img { height: 100%; From a9955ce80c1b73d92f60ed7ca9bacc64ac5ccfed Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 7 Dec 2017 22:34:56 +0200 Subject: [PATCH 216/385] merge fixes --- client/apps/city_issues/static/city_issues/css/style.css | 2 +- client/apps/city_issues/templates/issues/issues.html | 1 + client/apps/city_issues/templates/map_page.html | 4 ++-- client/apps/city_issues/templates/user/user.html | 2 ++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/apps/city_issues/static/city_issues/css/style.css b/client/apps/city_issues/static/city_issues/css/style.css index d8dd866..282e2dc 100644 --- a/client/apps/city_issues/static/city_issues/css/style.css +++ b/client/apps/city_issues/static/city_issues/css/style.css @@ -63,7 +63,7 @@ padding: 0; } -.message_box .error { +.message_box .some_error { font-size: 16px; color: red; diff --git a/client/apps/city_issues/templates/issues/issues.html b/client/apps/city_issues/templates/issues/issues.html index 798f98e..69d926c 100644 --- a/client/apps/city_issues/templates/issues/issues.html +++ b/client/apps/city_issues/templates/issues/issues.html @@ -95,5 +95,6 @@

    Add new issue

    {% load static %} + {% endblock extra_js %} diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index c1c0cb9..d2abcf4 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -85,8 +85,8 @@

    Issue description

    {% endblock content %} diff --git a/client/apps/city_issues/templates/user/user.html b/client/apps/city_issues/templates/user/user.html index 65b3585..e0ee467 100644 --- a/client/apps/city_issues/templates/user/user.html +++ b/client/apps/city_issues/templates/user/user.html @@ -156,6 +156,8 @@ {% block extra_js %} + {% load static %} + {% endblock extra_js %} diff --git a/client/apps/city_issues/views.py b/client/apps/city_issues/views.py index 96a84eb..2e0b871 100644 --- a/client/apps/city_issues/views.py +++ b/client/apps/city_issues/views.py @@ -61,41 +61,21 @@ def post(self, request): form = EditUserForm(data=request.POST, instance=request.user) if form.is_valid(): - if self.is_not_empty_passwords(form.cleaned_data) and self.check_passwords(user, form.cleaned_data): - - user.set_password(form.cleaned_data['confirm_password']) - user.name = form.cleaned_data['name'] user.alias = form.cleaned_data['alias'] user.email = form.cleaned_data['email'] + user.set_password(form.cleaned_data['confirm_password']) user.save() update_session_auth_hash(request, user) messages.success(request, 'Changes successfully saved') else: messages.error(request, form.errors) + return render(request, self.template_name, + {'form': form, 'has_error': 'error'}) return redirect(self.success_url) - def check_passwords(self, user, form_data): - if not user.check_password(form_data['current_password']): - messages.error(self.request, 'Wrong current password') - return redirect(self.success_url) - - elif not form_data['new_password'] == form_data['confirm_password']: - messages.error( - self.request, "New and confirm password don't match") - return redirect(self.success_url) - - else: - return True - - def is_not_empty_passwords(self, data): - if data['current_password'] == '' and data['new_password'] == '' and data['confirm_password'] == '': - return False - else: - return True - class IssueCreate(CreateView): """Create new issue""" From bfba9397a68c9f4a09effe6b18281f5e1d450e7a Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 10 Dec 2017 17:56:01 +0200 Subject: [PATCH 220/385] map UI upgrades --- backend/insert_db_data.py | 93 ++++++++++---- backend/models/issues.py | 16 +-- client/apps/city_issues/forms/forms.py | 1 + .../static/city_issues/css/style.css | 66 +++++++--- .../img/category_1_marker-icon.png | Bin 31677 -> 5257 bytes .../img/category_2_marker-icon.png | Bin 22389 -> 5143 bytes .../img/category_3_marker-icon.png | Bin 17749 -> 5376 bytes .../img/category_4_marker-icon.png | Bin 0 -> 7109 bytes .../city_issues/static/city_issues/js/main.js | 118 +++++++++--------- .../apps/city_issues/templates/map_page.html | 26 ++-- client/apps/city_issues/views.py | 32 ++++- 11 files changed, 231 insertions(+), 121 deletions(-) create mode 100644 client/apps/city_issues/static/city_issues/img/category_4_marker-icon.png diff --git a/backend/insert_db_data.py b/backend/insert_db_data.py index d15cc70..5932dc8 100644 --- a/backend/insert_db_data.py +++ b/backend/insert_db_data.py @@ -17,9 +17,10 @@ role1 = Role(role='moderator') role2 = Role(role='user') -category = Category(category='road accident', favicon='') -category1 = Category(category='infrastructure accident', favicon='') -category2 = Category(category='another accident', favicon='') +category = Category(category='Road accident', favicon='') +category1 = Category(category='Infrastructure accident', favicon='') +category2 = Category(category='Another accident', favicon='') +category3 = Category(category='Accident with animals', favicon='') status1 = Status(status="new") status2 = Status(status="on moderation") @@ -34,30 +35,63 @@ password='321', role_id='3') -issue1 = Issue(title='First issue', user_id='2', category_id='1', location_lat='50.621945', - location_lon='26.249314', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='on moderation', +issue1 = Issue(title='Car crash', user_id='2', category_id='1', location_lat='50.620226734521204', + location_lon='26.239514350891117', description='Two cars find one way in same moment of time ....', status='on moderation', open_date='2017/11/15') -issue2 = Issue(title='Road accident', user_id='3', category_id='2', location_lat='50.623673', - location_lon='26.250182', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='new', +issue2 = Issue(title='Trolleybus is broken', user_id='3', category_id='1', location_lat='50.6190831458868', + location_lon='26.252110004425052', description='It is very heavy traffic in this place', status='new', open_date='2017/11/16') -issue3 = Issue(title='Dog lost', user_id='3', category_id='3', location_lat='50.622584', - location_lon='26.252468', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='new', +issue3 = Issue(title='Prohibited parking', user_id='3', category_id='1', location_lat='50.61954603035055', + location_lon='26.25116586685181', description='As you see prohibited parking detecting', status='new', open_date='2017/11/17') -issue4 = Issue(title='Road accident', user_id='1', category_id='1', location_lat='50.622836', - location_lon='26.246706', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='new', +issue4 = Issue(title='Stolen car wheels', user_id='1', category_id='1', location_lat='50.61487613411816', + location_lon='26.25116586685181', description='My car wheels have been stolen by some bastards ', status='open', open_date='2017/11/18') -issue5 = Issue(title='Test case 5', user_id='2', category_id='2', location_lat='50.619977', - location_lon='26.247521', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='open', - open_date='2017/11/19') -issue6 = Issue(title='Test case 6', user_id='3', category_id='3', location_lat='50.619453', - location_lon='26.251834', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='open', +issue5 = Issue(title='Heavy traffic', user_id='2', category_id='1', location_lat='50.61320139365915', + location_lon='26.239514350891117', description='All cars are here, because Soborna street is closed', status='open', + open_date='2017/11/19', close_date='2017/11/25') +issue6 = Issue(title='No electricity', user_id='3', category_id='2', location_lat='50.6250186130551', + location_lon='26.253225803375248', description='There is no electricity in that place after storm', status='open', open_date='2017/11/20') -issue7 = Issue(title='Test case 7', user_id='1', category_id='1', location_lat='50.618500', - location_lon='26.249838', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='on moderation', +issue7 = Issue(title='No water', user_id='1', category_id='2', location_lat='50.62209181346729', + location_lon='26.283631324768066', description='We dont have water for two days. Rivnevodocanal cant say nothing about it', status='open', open_date='2017/11/21') -issue8 = Issue(title='Test case 8', user_id='2', category_id='2', location_lat='50.619242', - location_lon='26.246233', description='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut vulputate tortor. Quisque a mauris lobortis, tempor nisi ac, volutpat libero. Ut eu magna finibus, tincidunt ligula eget, dictum enim. Vivamus lobortis semper lorem, sit amet tincidunt metus viverra a. Sed ullamcorper ullamcorper porttitor.', status='closed', - open_date='2017/11/22') +issue8 = Issue(title='Gas smell', user_id='2', category_id='2', location_lat='50.62229601469869', + location_lon='26.231789588928226', description='we have strong gas smell here, call 104', status='closed', + open_date='2016/11/22', close_date='2017/01/15') +issue9 = Issue(title='Fire alarm', user_id='3', category_id='2', location_lat='50.62601232218674', + location_lon='26.25416994094849', description='Somebody fire that building, you can see some foto ...', status='open', + open_date='2017/11/23') +issue10 = Issue(title='Broken tree', user_id='1', category_id='3', location_lat='50.615978979420014', + location_lon='26.26311779022217', description='Old tree finally falls, you cant run and walk there', status='open', + open_date='2017/11/24') +issue11 = Issue(title='Street musician', user_id='2', category_id='3', location_lat='50.62004975238461', + location_lon='26.24080181121826', description='Very beautiful music', status='on moderation', + open_date='2017/11/26') +issue12 = Issue(title='Bad company', user_id='3', category_id='3', location_lat='50.607700200565034', + location_lon='26.231789588928226', description='I can see very suspicious there every day, beware', status='closed', + open_date='2014/11/27', close_date='2014/12/01') +issue13 = Issue(title='Beer fest', user_id='1', category_id='3', location_lat='50.617095413757845', + location_lon='26.255307197570804', description='Beer fest is running here, come on', status='on moderation', + open_date='2017/11/28') +issue14 = Issue(title='Prankers', user_id='2', category_id='3', location_lat='50.63607074324129', + location_lon='26.268010139465332', description='Some prankers offer to buy brick, but I have two already', status='open', + open_date='2017/11/29') +issue15 = Issue(title='Dog lost', user_id='3', category_id='4', location_lat='50.63979957034144', + location_lon='26.265778541564945', description='My lovely Rex disappeared in the dark, please help me find it.', status='closed', + open_date='2017/11/30', close_date='2017/12/10') +issue16 = Issue(title='Cat lost', user_id='1', category_id='4', location_lat='50.61943711676894', + location_lon='26.283631324768066', description='Big dark cat, cats nickname is Rambo, please return it for reward', status='open', + open_date='2017/12/01') +issue17 = Issue(title='Dog found', user_id='2', category_id='4', location_lat='50.63895584704026', + location_lon='26.206941604614258', description='Hungry and sick, anybody know it ?', status='open', + open_date='2017/12/02') +issue18 = Issue(title='Poor puppies', user_id='3', category_id='4', location_lat='50.63127999106349', + location_lon='26.20951652526856', description='We have four puppies to your lonely heart', status='new', + open_date='2017/12/03') +issue19 = Issue(title='Ugly hounds', user_id='1', category_id='4', location_lat='50.634083729153225', + location_lon='26.263289451599125', description='Beware, its dangerous to go there', status='on moderation', + open_date='2017/12/04') attachment1 = Attachment(issue_id='1', image_url=None) @@ -83,20 +117,27 @@ comment1 = Comments(user_id='1', issue_id='1', date_public='2017/09/26', comment='Vestibulum et scelerisque felis. Nunc elementum commodo enim non maximus. Vestibulum sed molestie sem. Maecenas aliquam at eros at mattis. Maecenas vitae suscipit turpis, at iaculis sem. Donec sagittis mauris sapien. Duis ultricies odio ac libero tincidunt convallis eget non dui. Quisque condimentum vel justo eget ultrices. In efficitur laoreet velit. ') comment2 = Comments(user_id='2', issue_id='1', date_public='2017/10/10', comment='Nunc elementum commodo enim non maximus. Vestibulum sed molestie sem. Maecenas aliquam at eros at mattis. Maecenas vitae suscipit turpis, at iaculis sem. Donec sagittis mauris sapien. Duis ultricies odio ac libero tincidunt convallis eget non dui. Quisque condimentum vel justo eget ultrices. In efficitur laoreet velit. ') -comment3 = Comments(user_id='3', issue_id='1', date_public='2017/11/06', comment='Maecenas vitae suscipit turpis, at iaculis sem. Donec sagittis mauris sapien. Duis ultricies odio ac libero tincidunt convallis eget non dui. Quisque condimentum vel justo eget ultrices. In efficitur laoreet velit. ') -comment4 = Comments(user_id='2', issue_id='3', date_public='2017/10/16', comment='Vivamus mauris massa, ullamcorper vel faucibus vitae, mollis at dolor. Vivamus fringilla ex ac pretium rhoncus. Integer dictum ligula est, vitae dictum metus condimentum tempus.') -comment5 = Comments(user_id='3', issue_id='3', date_public='2017/10/16', comment='Sed bibendum arcu eu nisi vestibulum, consectetur hendrerit mi pharetra. Vivamus mauris massa, ullamcorper vel faucibus vitae, mollis at dolor. Vivamus fringilla ex ac pretium rhoncus. Integer dictum ligula est, vitae dictum metus condimentum tempus.') +comment3 = Comments(user_id='3', issue_id='1', date_public='2017/11/06', + comment='Maecenas vitae suscipit turpis, at iaculis sem. Donec sagittis mauris sapien. Duis ultricies odio ac libero tincidunt convallis eget non dui. Quisque condimentum vel justo eget ultrices. In efficitur laoreet velit. ') +comment4 = Comments(user_id='2', issue_id='3', date_public='2017/10/16', + comment='Vivamus mauris massa, ullamcorper vel faucibus vitae, mollis at dolor. Vivamus fringilla ex ac pretium rhoncus. Integer dictum ligula est, vitae dictum metus condimentum tempus.') +comment5 = Comments(user_id='3', issue_id='3', date_public='2017/10/16', + comment='Sed bibendum arcu eu nisi vestibulum, consectetur hendrerit mi pharetra. Vivamus mauris massa, ullamcorper vel faucibus vitae, mollis at dolor. Vivamus fringilla ex ac pretium rhoncus. Integer dictum ligula est, vitae dictum metus condimentum tempus.') def db_insert_data(): """This function insert database data""" db.session.add_all([role, role1, role2, - category, category1, category2, + category, category1, category2, category3, status1, status2, status3, status4, user1, user2, user3, issue1, issue2, issue3, issue4, issue5, issue6, - issue7, issue8, + issue7, issue8, issue9, + issue10, issue11, issue12, + issue13, issue14, issue15, + issue16, issue17, issue18, + issue19, issueHistory1, issueHistory2, issueHistory3, issueHistory4, issueHistory5, issueHistory6, issueHistory7, diff --git a/backend/models/issues.py b/backend/models/issues.py index 71f5d9f..7d64240 100644 --- a/backend/models/issues.py +++ b/backend/models/issues.py @@ -38,7 +38,7 @@ class IssueHistory(db.Model): user_id = db.Column(db.ForeignKey(u'users.id')) issue_id = db.Column(db.ForeignKey(u'issues.id'), index=True) status_id = db.Column(db.ForeignKey(u'statuses.id'), index=True) - transaction_date = db.Column(db.TIMESTAMP) + transaction_date = db.Column(db.TIMESTAMP(timezone=True)) issue = db.relationship(u'Issue') status = db.relationship(u'Status') @@ -59,9 +59,9 @@ class Issue(db.Model): location_lon = db.Column(db.Float) status = db.Column(db.Text) description = db.Column(db.Text) - open_date = db.Column(db.TIMESTAMP) - close_date = db.Column(db.TIMESTAMP) - delete_date = db.Column(db.TIMESTAMP) + open_date = db.Column(db.TIMESTAMP(timezone=True)) + close_date = db.Column(db.TIMESTAMP(timezone=True)) + delete_date = db.Column(db.TIMESTAMP(timezone=True)) category = db.relationship(u'Category') user = db.relationship(u'User') @@ -98,7 +98,7 @@ class Comments(db.Model): __tablename__ = 'comments' id = db.Column(db.Integer, primary_key=True) comment = db.Column(db.Text) - date_public = db.Column(db.TIMESTAMP) + date_public = db.Column(db.TIMESTAMP(timezone=True)) user_id = db.Column(db.ForeignKey(u'users.id')) issue_id = db.Column(db.ForeignKey(u'issues.id'), index=True) @@ -121,8 +121,10 @@ def get_all_issue_history(issue_id): Comments.issue_id == issue_id).order_by(Comments.date_public).all() list_history = [] for histor in history: - list_history.append(['change_status', histor.status.status, histor.user.alias, histor.transaction_date.strftime('%Y-%m-%d %H:%M')]) + list_history.append(['change_status', histor.status.status, + histor.user.alias, histor.transaction_date.strftime('%Y-%m-%d %H:%M')]) for comment in comments: - list_history.append(['add_comment', comment.user.alias, comment.comment, comment.date_public.strftime('%Y-%m-%d %H:%M')]) + list_history.append(['add_comment', comment.user.alias, + comment.comment, comment.date_public.strftime('%Y-%m-%d %H:%M')]) list_history.sort(key=lambda history: history[3]) return list_history diff --git a/client/apps/city_issues/forms/forms.py b/client/apps/city_issues/forms/forms.py index 28ede30..2c032a0 100644 --- a/client/apps/city_issues/forms/forms.py +++ b/client/apps/city_issues/forms/forms.py @@ -88,6 +88,7 @@ class IssueFilter(forms.Form): widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})) show_closed = forms.BooleanField( + label="Closed only", required=False, initial=False, widget=forms.CheckboxInput()) diff --git a/client/apps/city_issues/static/city_issues/css/style.css b/client/apps/city_issues/static/city_issues/css/style.css index 282e2dc..12b8188 100644 --- a/client/apps/city_issues/static/city_issues/css/style.css +++ b/client/apps/city_issues/static/city_issues/css/style.css @@ -1,19 +1,22 @@ .map_container { - height: 900px; + min-height: 600px; + height: calc(100vh - 52px); } .issue_container { - position: fixed; - top: 72px; + box-sizing: border-box; + position: absolute; + top: 50px; right: 0; display: none; box-sizing: border-box; - width: 400px; - height: 100%; - padding: 20px; + width: 360px; + min-height: 600px; + height: calc(100% - 50px); background-color: #fff; + box-shadow: -5px 0 5px rgba(0, 0, 0, 0.3); z-index: 999; } @@ -41,16 +44,15 @@ } .issue_form-box { - position: fixed; - top: 52px; - left: 5%; - - padding: 5px; - + position: absolute; background-color: #fff; box-shadow: 0 0 3px 0 #000; + border-radius: 4px; + z-index: 1000; +} - z-index: 999; +.issue_form-container { + padding: 10px; } .message_box { @@ -71,4 +73,40 @@ .issue_form-container { display: none; -} \ No newline at end of file +} + +.issue_filter-form-show-btn { + background-color: #fff; + border: 4px solid #fff; + border-radius: 4px; + box-shadow: 0 1px 5px rgba(0,0,0,0.65); +} + +.issue_filter-form-show-btn:hover { + background-color: #f4f4f4; + border-color: #f4f4f4; +} + +.width.map-width { + margin: 0; +} + +.navbar { + margin-bottom: 0; +} + +.issue_header-box { + padding: 20px; + + color: #fff; + background-color: #4285F4; +} + +.issue_description { + padding: 20px; +} + +.issue_buttons-box { + padding: 20px; +} + diff --git a/client/apps/city_issues/static/city_issues/img/category_1_marker-icon.png b/client/apps/city_issues/static/city_issues/img/category_1_marker-icon.png index dc1898e4fec1851f8a735a8707253087e7e3f78a..bed5e39d8ec1fde6ffdc438d4645fd165cb06b7b 100644 GIT binary patch literal 5257 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 literal 31677 zcmXV1by!r-*S~akNiRq@NQ%Hx3ep`?3K&R9H%m7xASK-(EwR!dQj*e0cX#)D`TpKN zcpjLUJ9Fmb=bSTysjI%i!=}Op002)>K~56@kdYpLFww!0;dSe`0Dvv>Nmf?fQS+4y zL{VN=m|sj-m|K8{9{}9a(i2^^wWi5KhMGm1)#C`mzYc%?sEMqA*Q^lB!@-Y5___@a zFbd0Q+skRh7GMgnEy8kTW2+d$gOD_;c2Fth#=h8-QYR`}lu<4;xgQLqF3?~2e9mkh zn)`6zJ<$NbZv)VNDeGgwaI^JEmo_>>qar?-VIbii0w~tNqJ4F>4Eg0t

    DDsYvS- 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 diff --git a/client/apps/city_issues/static/city_issues/img/category_2_marker-icon.png b/client/apps/city_issues/static/city_issues/img/category_2_marker-icon.png index aa8e22af2a2227f5a5fd4716369aa81f9e9c141d..dfce30b659376368ae142a81caadfc2fa63bd9cb 100644 GIT binary patch literal 5143 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%0DGC4YbFENT~UL8LO^P>tj&VHv`2 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ÏnOj^6O!;FYFmqn8#&~ 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 literal 17749 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 0) { @@ -192,7 +185,7 @@ function IssueDescription(mapId, issueContainerId, issueCloseId) { } var editBtn = document.getElementById("issue_edit"); var dataUrl = editBtn.getAttribute("data-url").slice(0,-1); - editBtn.setAttribute("href", dataUrl + jsonData.id); + editBtn.setAttribute("href", dataUrl + issue_id); editBtn.style.display = "none"; if (jsonData.editable) { editBtn.style.display = "inline-block"; @@ -207,7 +200,7 @@ function IssueDescription(mapId, issueContainerId, issueCloseId) { xml.send(); xml.onload = function(){ var response = JSON.parse(JSON.parse(xml.responseText).slice(1,-1)); - current.insertIssueData(response); + current.insertIssueData(response, issue_id); }; }; @@ -242,6 +235,15 @@ function insertTemplate(parentId, templateId) { } } +function placeFilter() { + var leafletControls = document.querySelector(".leaflet-top.leaflet-left"); + var filterForm = document.querySelector("#issue_form-box"); + + leafletControlsCoordinatse = leafletControls.getBoundingClientRect(); + filterForm.style.top = (leafletControlsCoordinatse.bottom + 5) + "px"; + filterForm.style.left = (leafletControlsCoordinatse.left + 5) + "px"; +} + issueMap = new IssueMap("mapid"); issueMap.setFilterFromBtn("#issue_filter-form-btn"); @@ -254,9 +256,9 @@ issueMap.addMapLayer( '© OpenStreetMap'); issueMap.getMarkers("getissuesall/"); issueMap.addHandler(); -issueDescription = new IssueDescription("mapid", "issue_container", "issue_close"); -issueDescription.addHandler(); + insertTemplate("#message_box", "#message_list"); +placeFilter(); })(); diff --git a/client/apps/city_issues/templates/map_page.html b/client/apps/city_issues/templates/map_page.html index d2abcf4..6d0b33e 100644 --- a/client/apps/city_issues/templates/map_page.html +++ b/client/apps/city_issues/templates/map_page.html @@ -8,6 +8,8 @@ + + {% load static %} @@ -16,7 +18,7 @@ {% block content %}

    -
    +
    {% if messages %}
      @@ -33,8 +35,8 @@
    -
    - +
    +

    {% csrf_token %} @@ -54,16 +56,17 @@
    -

    -

    -
    - +
    +
    +

    +

    -

    Issue description

    -
    Close
    - Edit +
    +
    Close
    + Edit +