diff --git a/lodel/__init__.py b/lodel/__init__.py index 84c3a11..41e364b 100644 --- a/lodel/__init__.py +++ b/lodel/__init__.py @@ -1,3 +1 @@ #-*- coding: utf-8 -*- - -from .utils.starter import init_lodel diff --git a/lodel/datasource/generic/migrationhandler.py b/lodel/datasource/generic/migrationhandler.py new file mode 100644 index 0000000..c7f6fef --- /dev/null +++ b/lodel/datasource/generic/migrationhandler.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +## @package lodel.datasource.migrationhandler.generic +# @brief A generic migration handler +# +# According to it, every moditification is possible +# + + +## Manage model changes +class GenericMigrationHandler(object): + + def __init__(self, debug=False): + self.debug = debug + + ## @brief Records a change in the EditorialModel and indicates whether or not it is possible to make it + # @note The states (initial_state and new_state) contains only fields that changes + def register_change(self, em, uid, initial_state, new_state): + if self.debug: + print("\n##############") + print("GenericMigrationHandler debug. Changes for component with uid %s :" % uid) + if initial_state is None: + print("Component creation (uid = %s): \n\t" % uid, new_state) + elif new_state is None: + print("Component deletion (uid = %s): \n\t" % uid, initial_state) + else: + field_list = set(initial_state.keys()).union(set(new_state.keys())) + for field_name in field_list: + str_chg = "\t%s " % field_name + if field_name in initial_state: + str_chg += "'" + str(initial_state[field_name]) + "'" + else: + str_chg += " creating " + str_chg += " => " + if field_name in new_state: + str_chg += "'" + str(new_state[field_name]) + "'" + else: + str_chg += " deletion " + print(str_chg) + + print("##############\n") + + ## @brief Not usefull for the moment + def register_model_state(self, em, state_hash): + if self.debug: + print("New EditorialModel state registered : '%s'" % state_hash) diff --git a/lodel/datasource/mongodb/datasource.py b/lodel/datasource/mongodb/datasource.py index 36e5431..7100d59 100644 --- a/lodel/datasource/mongodb/datasource.py +++ b/lodel/datasource/mongodb/datasource.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- + +import bson +from bson.son import SON +from collections import OrderedDict import pymongo from pymongo import MongoClient from pymongo.errors import BulkWriteError @@ -64,12 +68,11 @@ class MongoDbDataSource(GenericDataSource): # @param filters list : List of filters # @param rel_filters list : List of relational filters # @param order list : List of column to order. ex: order = [('title', 'ASC'),] - # @param group list : List of tupple representing the column to group together. ex: group = [('title', 'ASC'),] + # @param group list : List of tupple representing the column used as "group by" fields. ex: group = [('title', 'ASC'),] # @param limit int : Number of records to be returned # @param offset int: used with limit to choose the start record # @param instanciate bool : If true, the records are returned as instances, else they are returned as dict # @return list - # @todo Implement the grouping # @todo Implement the relations def select(self, target_cls, field_list, filters, rel_filters=None, order=None, group=None, limit=None, offset=0, instanciate=True): @@ -79,13 +82,41 @@ class MongoDbDataSource(GenericDataSource): query_result_ordering = utils.parse_query_order(order) if order is not None else None results_field_list = None if len(field_list) == 0 else field_list limit = limit if limit is not None else 0 - cursor = collection.find( - filter=query_filters, - projection=results_field_list, - skip=offset, - limit=limit, - sort=query_result_ordering - ) + + if group is None: + cursor = collection.find( + filter=query_filters, + projection=results_field_list, + skip=offset, + limit=limit, + sort=query_result_ordering + ) + else: + pipeline = list() + unwinding_list = list() + grouping_dict = OrderedDict() + sorting_list = list() + for group_param in group: + field_name = group_param[0] + field_sort_option = group_param[1] + sort_option = utils.MONGODB_SORT_OPERATORS_MAP[field_sort_option] + unwinding_list.append({'$unwind': '$%s' % field_name}) + grouping_dict[field_name] = '$%s' % field_name + sorting_list.append((field_name, sort_option)) + + sorting_list.extends(query_result_ordering) + + pipeline.append({'$match': query_filters}) + if results_field_list is not None: + pipeline.append({'$project': SON([{field_name: 1} for field_name in field_list])}) + pipeline.extend(unwinding_list) + pipeline.append({'$group': grouping_dict}) + pipeline.extend({'$sort': SON(sorting_list)}) + if offset > 0: + pipeline.append({'$skip': offset}) + if limit is not None: + pipeline.append({'$limit': limit}) + results = list() for document in cursor: results.append(document) diff --git a/lodel/datasource/mongodb/migration_handler.py b/lodel/datasource/mongodb/migration_handler.py index 7c68785..9301fc9 100644 --- a/lodel/datasource/mongodb/migration_handler.py +++ b/lodel/datasource/mongodb/migration_handler.py @@ -1 +1,41 @@ -# -*- coding: utf-8 -*- \ No newline at end of file +# -*- coding: utf-8 -*- + +from lodel.datasource.generic.migrationhandler import GenericMigrationHandler +from lodel.datasource.mongodb.datasource import MongoDbDataSource +from lodel.editorial_model.components import EmClass, EmField +class MigrationHandlerChangeError(Exception): + pass + +## @brief Modifies a MongoDb database given editorial model changes +class MongoDbMigrationHandler(GenericMigrationHandler): + + ## @brief constructs a MongoDbMigrationHandler + # @param conn_args dict : a dictionary containing connection options + # @param **kwargs : extra arguments given to the connection methods + def __init__(self, conn_args=None, **kwargs): + if conn_args is None: + conn_args = {} # TODO : récupérer les options de connexion dans les settings + self.connection_name = conn_args['name'] + # del conn_args['module'] + + self.db_conn = MongoDbDataSource(self.connection_name) + # TODO Réimplémenter la partie sur les settings + mh_settings = {} + self.dryrun = kwargs['dryrun'] if 'dryrun' in kwargs else mh_settings['dryrun'] + self.foreign_keys = kwargs['foreign_keys'] if 'foreign_keys' in kwargs else mh_settings['foreign_keys'] + self.drop_if_exists = kwargs['drop_if_exists'] if 'drop_if_exists' in kwargs else mh_settings['drop_if_exists'] + self._create_default_collections(self.drop_if_exists) + + ## @brief Modify the database given an EM change + # + # @param em model : The EditorialModel.model object to provide the global context. + # @param uid str : The uid of the changed component. + # @param initial_state dict|None : dict with field name as key and field value as value. Represents the original state. None means it's a creation of a new component. + # @param new_state dict|None : dict with field name as key and field value as value. Represents the new state. None means it's a component deletion. + # @throw MigrationHandlerChangeError if the change was refused + def register_change(self, em, uid, initial_state, new_state): + pass + + + def _create_default_collections(self, drop_if_exist=False): + pass diff --git a/lodel/datasource/mongodb/utils.py b/lodel/datasource/mongodb/utils.py index 5935df2..f67c3ee 100644 --- a/lodel/datasource/mongodb/utils.py +++ b/lodel/datasource/mongodb/utils.py @@ -25,6 +25,10 @@ LODEL_SORT_OPERATORS_MAP = { 'DESC': pymongo.DESCENDING } +MONGODB_SORT_OPERATORS_MAP = { + 'ASC': 1, + 'DESC': -1 +} ## @brief Returns a collection name given a Emclass name # @param class_name str : The class name