diff --git a/ereuse_devicehub/forms.py b/ereuse_devicehub/forms.py index d88c9cf1..0f4cefbe 100644 --- a/ereuse_devicehub/forms.py +++ b/ereuse_devicehub/forms.py @@ -1,7 +1,9 @@ +from flask import g from flask_wtf import FlaskForm from werkzeug.security import generate_password_hash from wtforms import BooleanField, EmailField, PasswordField, validators +from ereuse_devicehub.db import db from ereuse_devicehub.resources.user.models import User @@ -59,3 +61,43 @@ class LoginForm(FlaskForm): self.form_errors.append(self.error_messages['inactive']) return user.is_active + + +class PasswordForm(FlaskForm): + password = PasswordField( + 'Current Password', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + ) + newpassword = PasswordField( + 'New Password', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + ) + renewpassword = PasswordField( + 'Re-enter New Password', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + ) + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + return False + + if not g.user.check_password(self.password.data): + return False + + if self.newpassword.data != self.renewpassword.data: + return False + + return True + + def save(self, commit=True): + g.user.password = self.newpassword.data + + db.session.add(g.user) + if commit: + db.session.commit() + return diff --git a/ereuse_devicehub/resources/agent/models.py b/ereuse_devicehub/resources/agent/models.py index ab9e073a..826d0545 100644 --- a/ereuse_devicehub/resources/agent/models.py +++ b/ereuse_devicehub/resources/agent/models.py @@ -3,7 +3,9 @@ from operator import attrgetter from uuid import uuid4 from citext import CIText -from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint +from sqlalchemy import Column +from sqlalchemy import Enum as DBEnum +from sqlalchemy import ForeignKey, Unicode, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import backref, relationship, validates @@ -31,7 +33,7 @@ class Agent(Thing): name = Column(CIText()) name.comment = """The name of the organization or person.""" tax_id = Column(Unicode(length=STR_SM_SIZE), check_lower('tax_id')) - tax_id.comment = """The Tax / Fiscal ID of the organization, + tax_id.comment = """The Tax / Fiscal ID of the organization, e.g. the TIN in the US or the CIF/NIF in Spain. """ country = Column(DBEnum(enums.Country)) @@ -42,7 +44,7 @@ class Agent(Thing): __table_args__ = ( UniqueConstraint(tax_id, country, name='Registration Number per country.'), UniqueConstraint(tax_id, name, name='One tax ID with one name.'), - db.Index('agent_type', type, postgresql_using='hash') + db.Index('agent_type', type, postgresql_using='hash'), ) @declared_attr @@ -63,7 +65,9 @@ class Agent(Thing): @property def actions(self) -> list: # todo test - return sorted(chain(self.actions_agent, self.actions_to), key=attrgetter('created')) + return sorted( + chain(self.actions_agent, self.actions_to), key=attrgetter('created') + ) @validates('name') def does_not_contain_slash(self, _, value: str): @@ -76,15 +80,17 @@ class Agent(Thing): class Organization(JoinedTableMixin, Agent): - default_of = db.relationship(Inventory, - uselist=False, - lazy=True, - backref=backref('org', lazy=True), - # We need to use this as we cannot do Inventory.foreign -> Org - # as foreign keys can only reference to one table - # and we have multiple organization table (one per schema) - foreign_keys=[Inventory.org_id], - primaryjoin=lambda: Organization.id == Inventory.org_id) + default_of = db.relationship( + Inventory, + uselist=False, + lazy=True, + backref=backref('org', lazy=True), + # We need to use this as we cannot do Inventory.foreign -> Org + # as foreign keys can only reference to one table + # and we have multiple organization table (one per schema) + foreign_keys=[Inventory.org_id], + primaryjoin=lambda: Organization.id == Inventory.org_id, + ) def __init__(self, name: str, **kwargs) -> None: super().__init__(**kwargs, name=name) @@ -97,12 +103,17 @@ class Organization(JoinedTableMixin, Agent): class Individual(JoinedTableMixin, Agent): active_org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id)) - active_org = relationship(Organization, primaryjoin=active_org_id == Organization.id) + + active_org = relationship( + Organization, primaryjoin=active_org_id == Organization.id + ) user_id = Column(UUID(as_uuid=True), ForeignKey(User.id), unique=True) - user = relationship(User, - backref=backref('individuals', lazy=True, collection_class=set), - primaryjoin=user_id == User.id) + user = relationship( + User, + backref=backref('individuals', lazy=True, collection_class=set), + primaryjoin=user_id == User.id, + ) class Membership(Thing): @@ -110,20 +121,29 @@ class Membership(Thing): For example, because the individual works in or because is a member of. """ - id = Column(Unicode(), check_lower('id')) - organization_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True) - organization = relationship(Organization, - backref=backref('members', collection_class=set, lazy=True), - primaryjoin=organization_id == Organization.id) - individual_id = Column(UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True) - individual = relationship(Individual, - backref=backref('member_of', collection_class=set, lazy=True), - primaryjoin=individual_id == Individual.id) - def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None: - super().__init__(organization=organization, - individual=individual, - id=id) + id = Column(Unicode(), check_lower('id')) + organization_id = Column( + UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True + ) + organization = relationship( + Organization, + backref=backref('members', collection_class=set, lazy=True), + primaryjoin=organization_id == Organization.id, + ) + individual_id = Column( + UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True + ) + individual = relationship( + Individual, + backref=backref('member_of', collection_class=set, lazy=True), + primaryjoin=individual_id == Individual.id, + ) + + def __init__( + self, organization: Organization, individual: Individual, id: str = None + ) -> None: + super().__init__(organization=organization, individual=individual, id=id) __table_args__ = ( UniqueConstraint(id, organization_id, name='One member id per organization.'), @@ -134,6 +154,7 @@ class Person(Individual): """A person in the system. There can be several persons pointing to a real. """ + pass diff --git a/ereuse_devicehub/resources/user/models.py b/ereuse_devicehub/resources/user/models.py index 70f14e00..5eadb21d 100644 --- a/ereuse_devicehub/resources/user/models.py +++ b/ereuse_devicehub/resources/user/models.py @@ -2,37 +2,44 @@ from uuid import uuid4 from flask import current_app as app from flask_login import UserMixin -from sqlalchemy import Column, Boolean, BigInteger, Sequence +from sqlalchemy import BigInteger, Boolean, Column, Sequence from sqlalchemy.dialects.postgresql import UUID from sqlalchemy_utils import EmailType, PasswordType from teal.db import IntEnum from ereuse_devicehub.db import db +from ereuse_devicehub.resources.enums import SessionType from ereuse_devicehub.resources.inventory.model import Inventory from ereuse_devicehub.resources.models import STR_SIZE, Thing -from ereuse_devicehub.resources.enums import SessionType class User(UserMixin, Thing): __table_args__ = {'schema': 'common'} id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True) email = Column(EmailType, nullable=False, unique=True) - password = Column(PasswordType(max_length=STR_SIZE, - onload=lambda **kwargs: dict( - schemes=app.config['PASSWORD_SCHEMES'], - **kwargs - ))) + password = Column( + PasswordType( + max_length=STR_SIZE, + onload=lambda **kwargs: dict( + schemes=app.config['PASSWORD_SCHEMES'], **kwargs + ), + ) + ) token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False) active = Column(Boolean, default=True, nullable=False) phantom = Column(Boolean, default=False, nullable=False) - inventories = db.relationship(Inventory, - backref=db.backref('users', lazy=True, collection_class=set), - secondary=lambda: UserInventory.__table__, - collection_class=set) + inventories = db.relationship( + Inventory, + backref=db.backref('users', lazy=True, collection_class=set), + secondary=lambda: UserInventory.__table__, + collection_class=set, + ) # todo set restriction that user has, at least, one active db - def __init__(self, email, password=None, inventories=None, active=True, phantom=False) -> None: + def __init__( + self, email, password=None, inventories=None, active=True, phantom=False + ) -> None: """Creates an user. :param email: :param password: @@ -44,8 +51,13 @@ class User(UserMixin, Thing): create during the trade actions """ inventories = inventories or {Inventory.current} - super().__init__(email=email, password=password, inventories=inventories, - active=active, phantom=phantom) + super().__init__( + email=email, + password=password, + inventories=inventories, + active=active, + phantom=phantom, + ) def __repr__(self) -> str: return ''.format(self) @@ -73,8 +85,8 @@ class User(UserMixin, Thing): @property def get_full_name(self): - # TODO(@slamora) create first_name & last_name fields and use - # them to generate user full name + # TODO(@slamora) create first_name & last_name fields??? + # needs to be discussed related to Agent <--> User concepts return self.email def check_password(self, password): @@ -84,9 +96,12 @@ class User(UserMixin, Thing): class UserInventory(db.Model): """Relationship between users and their inventories.""" + __table_args__ = {'schema': 'common'} user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id), primary_key=True) - inventory_id = db.Column(db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True) + inventory_id = db.Column( + db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True + ) class Session(Thing): @@ -96,9 +111,11 @@ class Session(Thing): token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False) type = Column(IntEnum(SessionType), default=SessionType.Internal, nullable=False) user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id)) - user = db.relationship(User, - backref=db.backref('sessions', lazy=True, collection_class=set), - collection_class=set) + user = db.relationship( + User, + backref=db.backref('sessions', lazy=True, collection_class=set), + collection_class=set, + ) def __str__(self) -> str: return '{0.token}'.format(self) diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js index c230363d..6cb71534 100644 --- a/ereuse_devicehub/static/js/main_inventory.js +++ b/ereuse_devicehub/static/js/main_inventory.js @@ -194,39 +194,28 @@ async function processSelectedDevices() { /** * Manage the actions that will be performed when applying the changes - * @param {*} ev event (Should be a checkbox type) - * @param {string} lotID lot id - * @param {number} deviceID device id + * @param {EventSource} ev event (Should be a checkbox type) + * @param {Lot} lot lot id + * @param {Device[]} deviceList device id */ - manage(event, lotID, deviceListID) { + manage(event, lot, deviceListID) { event.preventDefault(); + const lotID = lot.id; const srcElement = event.srcElement.parentElement.children[0] - const {indeterminate} = srcElement; const checked = !srcElement.checked; - const found = this.list.filter(list => list.lotID == lotID)[0]; - const foundIndex = found != undefined ? this.list.findLastIndex(x => x.lotID == found.lotID) : -1; + const found = this.list.filter(list => list.lot.id == lotID)[0]; if (checked) { - if (found != undefined && found.type == "Remove") { - if (found.isFromIndeterminate == true) { - found.type = "Add"; - this.list[foundIndex] = found; - } else { - this.list = this.list.filter(list => list.lotID != lotID); - } + if (found && found.type == "Remove") { + found.type = "Add"; } else { - this.list.push({ type: "Add", lotID, devices: deviceListID, isFromIndeterminate: indeterminate }); + this.list.push({ type: "Add", lot, devices: deviceListID}); } - } else if (found != undefined && found.type == "Add") { - if (found.isFromIndeterminate == true) { - found.type = "Remove"; - this.list[foundIndex] = found; - } else { - this.list = this.list.filter(list => list.lotID != lotID); - } + } else if (found && found.type == "Add") { + found.type = "Remove"; } else { - this.list.push({ type: "Remove", lotID, devices: deviceListID, isFromIndeterminate: indeterminate }); + this.list.push({ type: "Remove", lot, devices: deviceListID}); } if (this.list.length > 0) { @@ -268,14 +257,14 @@ async function processSelectedDevices() { this.list.forEach(async action => { if (action.type == "Add") { try { - await Api.devices_add(action.lotID, action.devices); + await Api.devices_add(action.lot.id, action.devices.map(dev => dev.data)); this.notifyUser("Devices sucefully aded to selected lot/s", "", false); } catch (error) { this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true); } } else if (action.type == "Remove") { try { - await Api.devices_remove(action.lotID, action.devices); + await Api.devices_remove(action.lot.id, action.devices.map(dev => dev.data)); this.notifyUser("Devices sucefully removed from selected lot/s", "", false); } catch (error) { this.notifyUser("Fail to remove devices from selected lot/s", error.responseJSON.message, true); @@ -299,15 +288,13 @@ async function processSelectedDevices() { const tmpDiv = document.createElement("div") tmpDiv.innerHTML = newRequest - const oldTable = Array.from(document.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value) const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value) - for (let i = 0; i < oldTable.length; i++) { - if (!newTable.includes(oldTable[i])) { - // variable from device_list.html --> See: ereuse_devicehub\templates\inventory\device_list.html (Ln: 411) - table.rows().remove(i) + table.rows().dt.activeRows.forEach(row => { + if (!newTable.includes(row.querySelector("input").attributes["data-device-dhid"].value)) { + row.remove() } - } + }) } } @@ -345,16 +332,22 @@ async function processSelectedDevices() { break; } - doc.children[0].addEventListener("mouseup", (ev) => actions.manage(ev, id, selectedDevicesIDs)); - doc.children[1].addEventListener("mouseup", (ev) => actions.manage(ev, id, selectedDevicesIDs)); + doc.children[0].addEventListener("mouseup", (ev) => actions.manage(ev, lot, selectedDevices)); + doc.children[1].addEventListener("mouseup", (ev) => actions.manage(ev, lot, selectedDevices)); elementTarget.append(doc); } const listHTML = $("#LotsSelector") // Get selected devices - const selectedDevicesIDs = $.map($(".deviceSelect").filter(":checked"), (x) => parseInt($(x).attr("data"))); - if (selectedDevicesIDs.length <= 0) { + const selectedDevices = table.rows().dt.activeRows.filter(item => item.querySelector("input").checked).map(item => { + const child = item.childNodes[0].children[0] + const info = {} + Object.values(child.attributes).forEach(attrib => { info[attrib.nodeName] = attrib.nodeValue }) + return info + }) + + if (selectedDevices.length <= 0) { listHTML.html("
  • No devices selected
  • "); return; } @@ -369,7 +362,7 @@ async function processSelectedDevices() { try { listHTML.html("
  • ") - const devices = await Api.get_devices(selectedDevicesIDs); + const devices = await Api.get_devices(selectedDevices.map(dev => dev.data)); let lots = await Api.get_lots(); lots = lots.map(lot => { @@ -377,11 +370,11 @@ async function processSelectedDevices() { .filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0) .map(device => parseInt(device.id)); - switch (lot.devices.length) { + switch (lot.devices.length) { case 0: lot.state = "false"; break; - case selectedDevicesIDs.length: + case selectedDevices.length: lot.state = "true"; break; default: diff --git a/ereuse_devicehub/templates/ereuse_devicehub/user_profile.html b/ereuse_devicehub/templates/ereuse_devicehub/user_profile.html index 71ab39fa..fd8b8804 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/user_profile.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/user_profile.html @@ -27,248 +27,40 @@ -
    +
    -
    -
    About
    -

    Sunt est soluta temporibus accusantium neque nam maiores cumque temporibus. Tempora libero non est unde veniam est qui dolor. Ut sunt iure rerum quae quisquam autem eveniet perspiciatis odit. Fuga sequi sed ea saepe at unde.

    - -
    Profile Details
    - -
    -
    Full Name
    -
    Kevin Anderson
    -
    - -
    -
    Company
    -
    Lueilwitz, Wisoky and Leuschke
    -
    - -
    -
    Job
    -
    Web Designer
    -
    - -
    -
    Country
    -
    USA
    -
    - -
    -
    Address
    -
    A108 Adam Street, New York, NY 535022
    -
    - -
    -
    Phone
    -
    (436) 486-3538 x29071
    -
    - -
    -
    Email
    -
    k.anderson@example.com
    -
    - -
    - -
    - - -
    -
    - -
    - Profile -
    - - -
    -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    - - -
    - -
    - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - -
    - -
    -
    - -
    - -
    +
    -
    - + + {% for f in password_form %} + {% if f == password_form.csrf_token %} + {{ f }} + {% else %}
    - +
    - + {{ f }} + {% if f.errors %} +

    + {% for error in f.errors %} + {{ error }}
    + {% endfor %} +

    + {% endif %}
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - + {% endif %} + {% endfor %}
    diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index 3a6874a9..827c3b68 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -37,16 +37,20 @@
    {% if lot.is_temporary %} - + + {% if 1 == 2 %}{# #} Add supplier Add receiver + {% endif %}{# #} + Delete Lot + {% endif %}
    diff --git a/ereuse_devicehub/views.py b/ereuse_devicehub/views.py index 0c9b4361..1c975285 100644 --- a/ereuse_devicehub/views.py +++ b/ereuse_devicehub/views.py @@ -3,8 +3,9 @@ from flask import Blueprint from flask.views import View from flask_login import current_user, login_required, login_user, logout_user -from ereuse_devicehub import __version__ -from ereuse_devicehub.forms import LoginForm +from ereuse_devicehub import __version__, messages +from ereuse_devicehub.db import db +from ereuse_devicehub.forms import LoginForm, PasswordForm from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.utils import is_safe_url @@ -53,10 +54,30 @@ class UserProfileView(View): context = { 'current_user': current_user, 'version': __version__, + 'password_form': PasswordForm(), } + return flask.render_template(self.template_name, **context) +class UserPasswordView(View): + methods = ['POST'] + decorators = [login_required] + + def dispatch_request(self): + form = PasswordForm() + db.session.commit() + if form.validate_on_submit(): + form.save(commit=False) + messages.success('Reset user password successfully!') + else: + messages.error('Error modifying user password!') + + db.session.commit() + return flask.redirect(flask.url_for('core.user-profile')) + + core.add_url_rule('/login/', view_func=LoginView.as_view('login')) core.add_url_rule('/logout/', view_func=LogoutView.as_view('logout')) core.add_url_rule('/profile/', view_func=UserProfileView.as_view('user-profile')) +core.add_url_rule('/set_password/', view_func=UserPasswordView.as_view('set-password'))