diff --git a/biscd/biscd/froms.py b/biscd/biscd/froms.py index 516dc86..d0c464f 100644 --- a/biscd/biscd/froms.py +++ b/biscd/biscd/froms.py @@ -1,6 +1,8 @@ from flask_wtf import FlaskForm from wtforms import SubmitField, StringField, PasswordField, BooleanField -from wtforms.validators import DataRequired, Email, EqualTo, Length +from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError + +from .models import User class LoginForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) @@ -17,7 +19,7 @@ class RegistrationForm(FlaskForm): submit = SubmitField('Register') def validate_username(self, username): - user = User.query.filter_by(username=username.data).first() + user = User(username.data) if user is not None: raise ValidationError('Please use a different username.') diff --git a/biscd/biscd/models/__init__.py b/biscd/biscd/models/__init__.py index 780340d..5452de6 100644 --- a/biscd/biscd/models/__init__.py +++ b/biscd/biscd/models/__init__.py @@ -1,3 +1,6 @@ from .project import Project from .user import User from .yaml_serializable import YamlSerializable + +User.initialize() +Project.initialize() diff --git a/biscd/biscd/models/project.py b/biscd/biscd/models/project.py index db868c0..f45d541 100644 --- a/biscd/biscd/models/project.py +++ b/biscd/biscd/models/project.py @@ -8,9 +8,10 @@ class Project(YamlSerializable): return Path(config.config_dir()) / 'projects.yaml' @property + @classmethod def _yaml_object_name(self): return 'projects' def __init__(self, name=None, branche='master'): self.name = name - self.branche = branche \ No newline at end of file + self.branche = branche diff --git a/biscd/biscd/models/user.py b/biscd/biscd/models/user.py index 8fc1257..53ed511 100644 --- a/biscd/biscd/models/user.py +++ b/biscd/biscd/models/user.py @@ -1,17 +1,39 @@ from pathlib import Path +from werkzeug.security import generate_password_hash, check_password_hash + from biscd import config -from biscd.models import YamlSerializable +from .yaml_serializable import YamlSerializable +from biscd import login + +class User(YamlSerializable): -class Project(YamlSerializable): @property def _storage_file(self): return Path(config.config_dir()) / 'users.yaml' @property + @classmethod def _yaml_object_name(self): return 'users' - def __init__(self, name=None, password=None, email=None): + def __init__(self, id=None, name=None, email=None, password=None, password_hash=None): + super().__init__(id) self.name = name - self.password = password + self.password_hash = set_password(password, password_hash) self.email = email + + def set_password(password, password_hash): + if password_hash: + return password_hash + if password: + return generate_password_hash(password) + return None + + def check_password(self, password): + if not password: + return False + return check_password_hash(self.password_hash, password) + +@login.user_loader +def load_user(id): + return super.get(int(id)) diff --git a/biscd/biscd/models/yaml_serializable.py b/biscd/biscd/models/yaml_serializable.py index 5e1c75f..f7552c9 100644 --- a/biscd/biscd/models/yaml_serializable.py +++ b/biscd/biscd/models/yaml_serializable.py @@ -5,36 +5,56 @@ class MyMeta(metaclass=ABCMeta): required_attributes = [] def __call__(self, *args, **kwargs): + # pylint: disable=no-member obj = super(MyMeta, self).__call__(*args, **kwargs) for attr_name in obj.required_attributes: if not getattr(obj, attr_name): raise ValueError('required attribute (%s) not set' % attr_name) return obj -class YamlSerializable(object, metaclass=MyMeta): +class YamlSerializable(object): + __metaclass__ = MyMeta required_attributes = ['name'] + _id_counter = 0 + + @classmethod + def initialize(cls): + ymlserializables = cls._get_all_from_file() + cls._id_counter = max(ymlserializable.id for ymlserializable in ymlserializables) + 1 @abstractmethod - def __init__(self): - pass + def __init__(self, id = None): + self.id = self.set_id(id) + + @classmethod + def set_id(cls, id): + if id is not None: + return id + id = cls._id_counter + cls._id_counter += 1 + return id + @classmethod @property @abstractmethod - def _storage_file(self): + def _storage_file(cls): pass + @classmethod @property @abstractmethod - def _yaml_object_name(self): + def _yaml_object_name(cls): pass @property def config_dict(self): + # pylint: disable=no-member ymlserializable_dict = self.__dict__.copy() ymlserializable_dict.pop('name') return {self.name: ymlserializable_dict} def save(self): + # pylint: disable=no-member if self.name is None: raise TypeError("Name cannot be None") ymlserializables = self._get_all_from_file() @@ -46,19 +66,45 @@ class YamlSerializable(object, metaclass=MyMeta): self._save_all_to_file(ymlserializables) @classmethod - def get(cls, name): - ymlserializable_dict = next(ymlserializable for ymlserializable in cls._get_all_from_file() if [*ymlserializable][0] == name) - if ymlserializable_dict is None: + def get(cls, identifier): + if isinstance(identifier, int): + id = identifier + ymlserializable_dict = next( + ymlserializable for ymlserializable in + cls._get_all_from_file() if int(ymlserializable['id']) == id + ) + return cls._ymlserializable_from_dict(ymlserializable_dict) + + if isinstance(identifier, str): + name = identifier + ymlserializable_dict = next( + ymlserializable for ymlserializable in + cls._get_all_from_file() if [*ymlserializable][0] == name + ) + return cls._ymlserializable_from_dict(ymlserializable_dict) + + return None + + @classmethod + def _ymlserializable_from_dict(cls, ymldict): + if ymldict is None: return None - ymlserializable_name = [*ymlserializable_dict][0] - ymlserializable_dict = ymlserializable_dict.get(ymlserializable_name) - - ymlserializable_dict['name'] = ymlserializable_name - + + # Extract the name + ymlserializable_name = [*ymldict][0] + + # Step into object + ymldict = ymldict.get(ymlserializable_name) + + # Add name to dict + ymldict['name'] = ymlserializable_name + + # Create empty instance ymlserializable = cls() - ymlserializable.__dict__ = ymlserializable_dict - return ymlserializable + # Fill instance with dict + ymlserializable.__dict__ = ymldict + return ymlserializable @classmethod def list(cls): @@ -72,11 +118,15 @@ class YamlSerializable(object, metaclass=MyMeta): @classmethod def _get_all_from_file(cls): with open(cls._storage_file) as file: - ymlserializables = yaml.load(file, yaml.FullLoader).get(_yaml_object_name) - return ymlserializables + ymlserializables = yaml.load(file, yaml.FullLoader).get(cls._yaml_object_name) + + highest_id = max(ymlserializable.id for ymlserializable in ymlserializables) + 1 + if highest_id > cls._id_counter: cls._id_counter = highest_id + + return ymlserializables @classmethod def _save_all_to_file(cls, ymlserializables): - ymlserializables_object = {_yaml_object_name : ymlserializables} + ymlserializables_object = {cls._yaml_object_name : ymlserializables} with open(cls._storage_file, 'w') as file: yaml.dump(ymlserializables_object, file) diff --git a/biscd/biscd/routes.py b/biscd/biscd/routes.py index b87a22e..88cc31b 100644 --- a/biscd/biscd/routes.py +++ b/biscd/biscd/routes.py @@ -1,7 +1,7 @@ -from flask import render_template, flash, abort +from flask import render_template, flash, abort, redirect, request, url_for, url_parse from flask_login import current_user, login_user, logout_user, login_required from biscd import app -from .models import Project +from .models import Project, User from .froms import NewProjectForm, LoginForm @app.route('/', methods=['GET', 'POST']) @@ -22,7 +22,7 @@ def login(): return redirect(url_for('index')) form = LoginForm() if form.validate_on_submit(): - user = User.query.filter_by(username=form.username.data).first() + user = User.get(form.username.data) if user is None or not user.check_password(form.password.data): flash('Invalid username or password') return redirect(url_for('login'))