class ModelField_Base(DBResource): # Fields required = Boolean_Field(title=MSG(u'Required')) multiple = Boolean_Field(title=MSG(u'Multiple')) tip = Text_Field(title=MSG(u'Tip')) def get_owner(self): return self.parent.get_owner() def set_value(self, name, value, language=None): proxy = super(ModelField_Base, self) has_changed = proxy.set_value(name, value, language) if has_changed: class_id = str(self.parent.abspath) self.database._resources_registry.pop(class_id, None) return has_changed def get_field_kw(self, field): return { 'multiple': self.get_value('multiple'), 'required': self.get_value('required'), 'title': self.get_title(), 'widget': field.widget(tip=self.get_value('tip')) } # Views _fields = ['title', 'required', 'multiple', 'tip'] new_instance = AutoAdd(fields=_fields, automatic_resource_name=True) edit = AutoEdit(fields=_fields)
class WebPage(File): class_id = 'webpage' class_title = MSG(u'Web Page') class_description = MSG(u'Create and publish a Web Page.') class_icon16 = 'icons/16x16/html.png' class_icon48 = 'icons/48x48/html.png' data = HTMLFile_Field(title=MSG(u'Body')) ####################################################################### # API ####################################################################### def get_content_type(self): return 'application/xhtml+xml; charset=UTF-8' # XXX Obsolete: remove and call 'get_html_field_body_stream' instead def get_html_data(self, language=None): return self.get_html_field_body_stream('data', language) def to_text(self, languages=None): if languages is None: languages = self.get_root().get_value('website_languages') result = {} for language in languages: handler = self.get_value('data', language=language) if handler: result[language] = handler.to_text() return result ####################################################################### # Views ####################################################################### new_instance = AutoAdd(fields=['title', 'location', 'data']) view = WebPage_View
class Choice(DBResource): class_id = 'model-field-choice' class_title = MSG(u'Choice') # Views class_views = ['edit', 'commit_log'] new_instance = AutoAdd(fields=['title'])
class AccessRule(DBResource): class_id = 'config-access-rule' class_title = MSG(u'Access rule') # Fields group = Select_Field(required=True, title=MSG(u'User group'), datatype=Groups_Datatype, indexed=True, stored=True) permission = Permissions_Field(required=True, indexed=True, stored=True) search_path = Path_Field(indexed=True, stored=True) search_path_depth = PathDepth_Field() search_format = SearchFormat_Field(indexed=True, stored=True) # Views class_views = ['edit', 'results', 'commit_log'] _fields = [ 'group', 'permission', 'search_path', 'search_path_depth', 'search_format' ] new_instance = AutoAdd(fields=_fields, automatic_resource_name=True) edit = AutoEdit(fields=_fields) results = AccessRule_Results # API def get_search_query(self): permission = self.get_value('permission') if permission == 'add': names = ['path'] else: names = ['path', 'format'] # Query query = AndQuery() for name in names: field_name = 'search_%s' % name field = self.get_field(field_name) value = field.get_value(self, field_name) if not value: continue if name == 'path': depth = self.get_value('search_path_depth') depth = None if depth == '*' else int(depth) subquery = get_base_path_query(value, 0, depth) elif field.multiple: err = "access rules don't yet support multiple fields" raise NotImplementedError, err else: subquery = PhraseQuery(name, value) query.append(subquery) # Ok return query
class Group(DBResource): class_id = 'config-group' class_title = MSG(u'User Group') class_description = MSG(u'...') # Views class_views = ['browse_users', 'edit'] browse_users = Group_BrowseUsers() new_instance = AutoAdd(fields=['title'])
class DBResource(Resource): class_version = '20071215' class_description = None class_icon16 = 'icons/16x16/resource.png' class_icon48 = 'icons/48x48/resource.png' class_views = [] context_menus = [] def __init__(self, metadata): self.metadata = metadata def __eq__(self, resource): if not isinstance(resource, DBResource): error = "cannot compare DBResource and %s" % type(resource) raise TypeError, error return self.abspath == resource.abspath def __ne__(self, node): return not self.__eq__(node) ####################################################################### # API / Tree ####################################################################### @property def database(self): return self.metadata.database @lazy def parent(self): abspath = self.abspath if len(abspath) == 0: return None return self.get_resource(abspath[:-1]) @property def name(self): return self.abspath.get_name() def get_root(self): return self.get_resource('/') def get_pathto(self, resource): return self.abspath.get_pathto(resource.abspath) ####################################################################### # API / Folderish ####################################################################### __fixed_handlers__ = [] # Resources that cannot be removed @property def handler(self): cls = FolderHandler key = self.metadata.key[:-9] handler = self.database.get_handler(key, cls=cls, soft=True) if handler is None: handler = cls() self.database.push_phantom(key, handler) return handler def get_handlers(self): """Return all the handlers attached to this resource, except the metadata. """ handlers = [self.handler] # Fields for name, field in self.get_fields(): if issubclass(field, File_Field): value = field.get_value(self, name) if value is not None: handlers.append(value) # Ok return handlers def _get_names(self): folder = self.handler return [ x[:-9] for x in folder.get_handler_names() if x[-9:] == '.metadata' ] def get_names(self, path='.'): resource = self.get_resource(path) return resource._get_names() def get_resource(self, path, soft=False): if type(path) is not Path: path = Path(path) # 1. Get the metadata if path.is_absolute(): abspath = path else: abspath = self.abspath.resolve2(path) return self.database.get_resource(abspath, soft=soft) def get_resources(self, path='.'): here = self.get_resource(path) for name in here._get_names(): yield here.get_resource(name) def make_resource_name(self): max_id = -1 for name in self.get_names(): # Mixing explicit and automatically generated names is allowed try: id = int(name) except ValueError: continue if id > max_id: max_id = id return str(max_id + 1) def make_resource(self, name, cls, soft=False, **kw): # Automatic name if name is None: name = self.make_resource_name() # Make a resource somewhere else if '/' in name: path = dirname(name) name = basename(name) resource = self.get_resource(path) resource.make_resource(name, cls, soft=soft, **kw) return # Soft if soft is True: resource = self.get_resource(name, soft=True) if resource: return resource # Make the metadata metadata = Metadata(cls=cls) self.handler.set_handler('%s.metadata' % name, metadata) metadata.set_property('mtime', get_context().timestamp) # Initialize resource = self.get_resource(name) self.database.add_resource(resource) resource.init_resource(**kw) # Ok return resource def del_resource(self, name, soft=False, ref_action='restrict'): """ref_action allows to specify which action is done before deleting the resource. ref_action can take 2 values: - 'restrict' (default value): do an integrity check - 'force': do nothing """ database = self.database resource = self.get_resource(name, soft=soft) if soft and resource is None: return # Referential action if ref_action == 'restrict': # Check referencial-integrity (FIXME Check sub-resources too) path = str(resource.abspath) query_base_path = get_base_path_query(path) query = AndQuery(PhraseQuery('links', path), NotQuery(query_base_path)) results = database.search(query) # A resource may have been updated in the same transaction, # so not yet reindexed: we need to check that the resource # really links. for referrer in results.get_resources(): if path in referrer.get_links(): err = 'cannot delete, resource "%s" is referenced' raise ConsistencyError, err % path elif ref_action == 'force': # Do not check referencial-integrity pass else: raise ValueError, 'Incorrect ref_action "%s"' % ref_action # Events, remove path = str(resource.abspath) database.remove_resource(resource) # Remove fs = database.fs for handler in resource.get_handlers(): # Skip empty folders and phantoms if fs.exists(handler.key): database.del_handler(handler.key) self.handler.del_handler('%s.metadata' % name) # Clear cookie context = get_context() cut, paths = context.get_cookie('ikaaro_cp', datatype=CopyCookie) if path in paths: context.del_cookie('ikaaro_cp') def copy_resource(self, source, target): raise NotImplementedError def move_resource(self, source, target): raise NotImplementedError def traverse_resources(self): yield self for name in self._get_names(): resource = self.get_resource(name) for x in resource.traverse_resources(): yield x ####################################################################### # API / Views ####################################################################### def get_default_view_name(self): views = self.class_views if not views: return None context = get_context() for view_name in views: view = getattr(self, view_name, None) if context.is_access_allowed(self, view): return view_name return views[0] def get_view(self, name, query=None): # To define a default view, override this if name is None: name = self.get_default_view_name() if name is None: return None # Explicit view, defined by name view = getattr(self, name, None) if is_prototype(view, BaseView): context = get_context() view = view(resource=self, context=context) # bind return view return None def get_context_menus(self): return self.context_menus ######################################################################## # Properties ######################################################################## def get_value(self, name, language=None): field = self.get_field(name) if field is None: return None return field.get_value(self, name, language) def set_value(self, name, value, language=None, **kw): field = self.get_field(name) if field.multilingual and language is None: raise ValueError, 'Field %s is multilingual' % name if field is None: raise ValueError, 'Field %s do not exist' % name return field.set_value(self, name, value, language, **kw) def get_value_title(self, name, language=None): field = self.get_field(name) if field is None: return None return field.get_value_title(self, name, language) def get_brain_value(self, name): brain = get_context().database.search( PhraseQuery('abspath', str(self.abspath))).get_documents()[0] return getattr(brain, name, None) def get_html_field_body_stream(self, name, language=None): """Utility method, returns the stream for the given html field. """ # 1. Check it is an html-file field field = self.get_field(name) if not is_prototype(field, HTMLFile_Field): raise ValueError, 'expected html-file field' # 2. Get the handler handler = field.get_value(self, name, language) if not handler: handler = field.class_handler() # 3. Get the body body = handler.get_body() if not body: raise ValueError, 'html file does not have a body' return body.get_content_elements() def get_property(self, name, language=None): property = self.metadata.get_property(name, language=language) if property: return property field = self.get_field(name) if field is None: return None default = field.get_default() if field.multiple: return [Property(x) for x in default] return Property(default) # XXX Backwards compatibility set_property = set_value def get_page_title(self): return self.get_title() def init_resource(self, **kw): """Return a Metadata object with sensible default values. """ # Ownership owner = self.get_field('owner') if owner: user = get_context().user if user: self.set_value('owner', str(user.abspath)) # Keyword parameters for name, value in kw.items(): field = self.get_field(name) if field is None: raise ValueError, 'undefined field "%s"' % name if type(value) is dict: for lang in value: field._set_value(self, name, value[lang], lang) else: field._set_value(self, name, value) def load_handlers(self): self.get_handlers() ######################################################################## # Fields ######################################################################## mtime = Datetime_Field(indexed=True, stored=True, readonly=True) last_author = Char_Field(indexed=False, stored=True, readonly=True) title = Text_Field(indexed=True, stored=True, title=MSG(u'Title')) description = Textarea_Field(indexed=True, title=MSG(u'Description'), hidden_by_default=True) subject = Text_Field(indexed=True, title=MSG(u'Keywords'), hidden_by_default=True) share = Share_Field @property def is_content(self): return self.parent.is_content def has_property(self, name, language=None): return self.metadata.has_property(name, language=language) def del_property(self, name): if self.has_property(name): self.database.change_resource(self) self.metadata.del_property(name) ######################################################################## # Versioning ######################################################################## def get_files_to_archive(self, content=False): metadata = self.metadata.key if content is True: folder = self.handler.key return [metadata, folder] return [metadata] def get_revisions(self, n=None, content=False, author_pattern=None, grep_pattern=None): if self.parent is None and content is True: files = None else: files = self.get_files_to_archive(content) worktree = self.database.worktree return worktree.git_log(files, n, author_pattern, grep_pattern) def get_owner(self): return self.get_value('owner') def get_share(self): return self.get_value('share') ######################################################################## # Indexing ######################################################################## def to_text(self): """This function must return: 1) An unicode text. or 2) A dict in a multilingual context: {'fr': u'....', 'en': u'....' ....} """ raise NotImplementedError def get_catalog_values(self): values = {} # Step 1. Automatically index fields languages = self.get_root().get_value('website_languages') for name, field in self.get_fields(): if not field.indexed and not field.stored: continue if field.multilingual: value = {} for language in languages: value[language] = field.get_value(self, name, language) values[name] = value else: values[name] = field.get_value(self, name) # Step 2. Index non-metadata properties # Path related fields abspath = self.abspath values['abspath'] = str(abspath) n = len(abspath) values['abspath_depth'] = n if n: values['parent_paths'] = [str(abspath[:i]) for i in range(n)] values['name'] = self.name values['is_content'] = self.is_content # Class related fields values['format'] = self.metadata.format values['base_classes'] = [] for cls in self.__class__.__mro__: class_id = getattr(cls, 'class_id', None) if class_id: values['base_classes'].append(class_id) # Links to other resources values['owner'] = self.get_owner() values['share'] = self.get_share() values['links'] = list(self.get_links()) values['onchange_reindex'] = self.get_onchange_reindex() # Full text context = get_context() try: server = context.server except AttributeError: server = None if server is not None and server.index_text: try: values['text'] = self.to_text() except NotImplementedError: pass except Exception: log = 'Indexation failed: %s' % abspath log_warning(log, domain='ikaaro') # Time events reminder, payload = self.next_time_event() values['next_time_event'] = reminder values['next_time_event_payload'] = dumps(payload) # Ok return values def get_onchange_reindex(self): return None ####################################################################### # Time events ####################################################################### def next_time_event(self): return None, None def time_event(self, payload): raise NotImplementedError ####################################################################### # API ####################################################################### def rename_handlers(self, new_name): """Consider we want to rename this resource to the given 'new_name', return the old a new names for all the attached handlers (except the metadata). This method is required by the "move_resource" method. """ langs = self.get_resource('/').get_value('website_languages') aux = [(self.name, new_name)] for field_name in self.fields: field = self.get_field(field_name) if field and issubclass(field, File_Field): old = '%s.%s' % (self.name, field_name) new = '%s.%s' % (new_name, field_name) if field.multilingual: for language in langs: aux.append(('%s.%s' % (old, language), '%s.%s' % (new, language))) else: aux.append((old, new)) return aux def _on_move_resource(self, source): """This method updates the links from/to other resources. It is called when the resource has been moved and/or renamed. This method is called by 'Database._before_commit', the 'source' parameter is the place the resource has been moved from. """ # (1) Update links to other resources self.update_incoming_links(Path(source)) # (2) Update resources that link to me database = self.database target = self.abspath query = PhraseQuery('links', source) results = database.search(query).get_documents() for result in results: path = result.abspath path = database.resources_old2new.get(path, path) resource = self.get_resource(path) resource.update_links(source, target) def get_links(self): # Automatically from the fields languages = self.get_resource('/').get_value('website_languages') links = set() for field_name in self.fields: field = self.get_field(field_name) if field: field.get_links(links, self, field_name, languages) # Support for dynamic models class_id = self.metadata.format if class_id[0] == '/': links.add(class_id) # Ok return links def update_links(self, source, target): """The resource identified by 'source' is going to be moved to 'target'. Update our links to it. The parameters 'source' and 'target' are absolute 'Path' objects. """ base = str(self.abspath) old_base = self.database.resources_new2old.get(base, base) old_base = Path(old_base) new_base = Path(base) languages = self.get_resource('/').get_value('website_languages') for field_name in self.fields: field = self.get_field(field_name) if field: field.update_links(self, field_name, source, target, languages, old_base, new_base) self.database.change_resource(self) def update_incoming_links(self, source): """Update the relative links coming out from this resource after it was moved, so they are not broken. The old path is in parameter. The new path is "self.abspath". """ languages = self.get_resource('/').get_value('website_languages') for field_name in self.fields: field = self.get_field(field_name) if field: field.update_incoming_links(self, field_name, source, languages) ######################################################################## # Upgrade ######################################################################## def get_next_versions(self): cls_version = self.class_version obj_version = self.metadata.version # Set zero version if the resource does not have a version if obj_version is None: obj_version = '00000000' # Get all the version numbers versions = [] for cls in self.__class__.mro(): for name in cls.__dict__.keys(): if not name.startswith('update_'): continue kk, version = name.split('_', 1) if len(version) != 8: continue try: int(version) except ValueError: continue if version > obj_version and version <= cls_version: versions.append(version) versions.sort() return versions def update(self, version): # Action getattr(self, 'update_%s' % version)() # If the action removes the resource, we are done metadata = self.metadata if metadata.key is None: return # Update version metadata.set_changed() metadata.version = version ####################################################################### # Icons ####################################################################### @classmethod def get_class_icon(cls, size=16): icon = getattr(cls, 'class_icon%s' % size, None) if icon is None: return None return '/ui/%s' % icon @classmethod def get_resource_icon(cls, size=16): icon = getattr(cls, 'icon%s' % size, None) if icon is None: return cls.get_class_icon(size) return ';icon%s' % size def get_method_icon(self, view, size='16x16', **kw): icon = getattr(view, 'icon', None) if icon is None: return None if callable(icon): icon = icon(self, **kw) return '/ui/icons/%s/%s' % (size, icon) ####################################################################### # User interface ####################################################################### def get_views(self): context = get_context() for name in self.class_views: view_name = name.split('?')[0] view = self.get_view(view_name) if context.is_access_allowed(self, view): yield name, view def get_title(self, language=None): title = self.get_value('title', language=language) if title: return title # Fallback to the resource's name return unicode(self.name) def get_edit_languages(self, context): root = self.get_root() site_languages = root.get_value('website_languages') default = root.get_default_edit_languages() # Can not use context.query[] because edit_language is not necessarily # defined datatype = String(multiple=True, default=default) edit_languages = context.get_query_value('edit_language', datatype) edit_languages = [x for x in edit_languages if x in site_languages] return edit_languages if edit_languages else default ####################################################################### # Cut & Paste Resources ####################################################################### def can_paste(self, source): """Is the source resource can be pasted into myself. Question is "can I handle this type of resource?" """ raise NotImplementedError def can_paste_into(self, target): """Can I be pasted into the given target. Question is "Is this container compatible with myself?" """ # No restriction by default. Functional modules will want to keep # their specific resources for them. return True # Views new_instance = AutoAdd(fields=['title', 'location']) edit = AutoEdit(fields=['title', 'description', 'subject', 'share']) remove = DBResource_Remove get_file = DBResource_GetFile get_image = DBResource_GetImage # Login/Logout login = LoginView logout = LogoutView # Popups add_image = DBResource_AddImage add_link = DBResource_AddLink add_media = DBResource_AddMedia # Commit log commit_log = DBResource_CommitLog changes = DBResource_Changes # Links backlinks = DBResource_Backlinks links = DBResource_Links # External editor http_put = Put_View http_delete = Delete_View # Rest (web services) rest_login = Rest_Login rest_query = Rest_Query rest_create = Rest_Create rest_read = Rest_Read rest_update = Rest_Update rest_delete = Rest_Delete rest_schema = Rest_Schema
class MenuItem(OrderedFolder): class_id = 'config-menu-item' class_title = MSG(u'Menu') # Fields path = URI_Field(required=True, title=MSG(u'Path'), widget=PathSelectorWidget) target = Target_Field def get_document_types(self): return [MenuItem] # Views class_views = ['edit', 'browse_content', 'add_menu', 'commit_log'] _fields = ['title', 'path', 'target'] new_instance = AutoAdd(fields=_fields) edit = AutoEdit(fields=_fields) browse_content = MenuItem_Browse add_menu = AddMenu # API def _is_allowed_to_access(self, context, uri): # Get the reference and path ref, path, view = split_reference(uri) # Broken entry if ref is None or path == '': return False # External link if ref.scheme: return True # Broken link resource = self.get_resource(path, soft=True) if resource is None: return False if view: # Remove the first '/;' of the view view = resource.get_view(view[2:], ref.query) if not view: return False # Check ACL return context.is_access_allowed(resource, view) # Check if the user can access to resource views # get_views checks ACLs with by calling is_access_allowed resource_views = list(resource.get_views()) return len(resource_views) > 0 def get_menu_namespace_level(self, context, url, use_first_child=False): menu_abspath = self.abspath here = context.resource here_abspath = here.abspath here_view_name = url[-1] here_abspath_and_view = '%s/%s' % (here_abspath, here_view_name) items = [] for resource in self.get_resources_in_order(): uri = resource.get_value('path') if not self._is_allowed_to_access(context, uri): continue ref, path, view = split_reference(uri) title = resource.get_value('title') target = resource.get_value('target') # Case 1: External link if ref.scheme: items.append({ 'id': 'menu_%s' % resource.name, 'path': str(ref), 'real_path': None, 'title': title, 'description': None, 'in_path': False, 'active': False, 'class': None, 'target': target, 'items': []}) continue # Case 2: Internal link # Sub level subtabs = resource.get_menu_namespace_level(context, url, use_first_child) resource = self.get_resource(path, soft=True) item_id = 'menu_%s' % resource.name # Use first child by default we use the resource itself resource_path = path # Keep the real path to avoid highlight problems resource_original_path = path # use first child if use_first_child and subtabs: sub_path = subtabs[0]['real_path'] # if the first child is an external link => skip it if sub_path is not None: resource_path = sub_path # Set active, in_path active = in_path = False # add default view if view: resource_method = view[2:] item_id += '_%s' % resource_method else: resource_method = resource.get_default_view_name() resource_abspath_and_view = '%s/;%s' % (resource.abspath, resource_method) if here_abspath_and_view == resource_abspath_and_view: active = True else: # Use the original path for the highlight res_abspath = menu_abspath.resolve2(resource_original_path) common_prefix = here_abspath.get_prefix(res_abspath) # Avoid to always set the root entree 'in_path' # If common prefix equals root abspath set in_path to False # otherwise compare common_prefix and res_abspath if common_prefix != Path('/'): in_path = (common_prefix == res_abspath) # Build the new reference with the right path ref2 = deepcopy(ref) resource = self.get_resource(resource_path) ref2.path = context.get_link(resource) if view: ref2.path += view items.append({ 'id': item_id, 'path': str(ref2), 'real_path': resource.abspath, 'title': title, 'description': None, # FIXME 'in_path': active or in_path, 'active': active, 'class': None, 'target': target, 'items': subtabs}) # Set class x = None for i, item in enumerate(items): if item['active']: x = i break if item['in_path'] and x is None: x = i break if x is not None: items[x]['class'] = 'in-path' if len(items) > 0: # Add class "first" to the first item css = items[0]['class'] or '' items[0]['class'] = css + ' first' # Add class "last" to the last item css = items[-1]['class'] or '' items[-1]['class'] = css + ' last' return items
class DBResource(Resource): class_version = '20071215' class_description = None class_icon16 = '/ui/ikaaro/icons/16x16/resource.png' class_icon48 = '/ui/ikaaro/icons/48x48/resource.png' class_views = [] # Internal _values = {} _values_title = {} _metadata = None _brain = None # Config context_menus = [] # Fields uuid = UUID_Field() ctime = CTime_Field() mtime = MTime_Field() last_author = LastAuthor_Field() title = Title_Field() description = Description_Field() subject = Subject_Field() share = Share_Field() def __init__(self, abspath, database, metadata=None, brain=None): self.abspath = abspath self.database = database self._metadata = metadata self._brain = brain self._values = {} self._values_title = {} def __eq__(self, resource): if resource is None: return False if not isinstance(resource, DBResource): error = "cannot compare DBResource and %s" % type(resource) raise TypeError(error) return self.abspath == resource.abspath def __ne__(self, node): return not self.__eq__(node) ####################################################################### # API / Tree ####################################################################### @lazy def metadata(self): if self._metadata: return self._metadata self._metadata = self.database.get_metadata(self.abspath) return self._metadata @lazy def parent(self): abspath = self.abspath if len(abspath) == 0: return None return self.get_resource(abspath[:-1]) @property def name(self): return self.abspath.get_name() def get_root(self): return self.get_resource('/') def get_pathto(self, resource): return self.abspath.get_pathto(resource.abspath) ####################################################################### # API / Folderish ####################################################################### __fixed_handlers__ = [] # Resources that cannot be removed @property def handler(self): cls = FolderHandler key = self.metadata.key[:-9] handler = self.database.get_handler(key, cls=cls, soft=True) if handler is None: handler = cls() self.database.push_handler(key, handler) return handler def get_handlers(self): """Return all the handlers attached to this resource, except the metadata. """ handlers = [self.handler] return handlers + self.get_fields_handlers() def get_fields_handlers(self): handlers = [] root = self.get_root() langs = root.get_value('website_languages') # Fields for name, field in self.get_fields(): if is_prototype(field, File_Field): if field.multilingual: for language in langs: value = field.get_value(self, name, language) if value is not None: handlers.append(value) else: value = field.get_value(self, name) if value is not None: handlers.append(value) # Ok return handlers def get_resource(self, path, soft=False): if type(path) is not Path: path = Path(path) # 1. Get the metadata if path.is_absolute(): abspath = path else: abspath = self.abspath.resolve2(path) return self.database.get_resource(abspath, soft=soft) ####################################################################### # Resource API ####################################################################### def get_resources(self): for name in self._get_names(): yield self.get_resource(name) def get_resource_by_uuid(self, uuid, context, bases_class_id=None, class_id=None): # Get query query = get_resource_by_uuid_query(uuid, bases_class_id, class_id) search = context.database.search(query) # Return resource if not search: return None return search.get_resources(size=1).next() def make_resource_name(self): raise NotImplementedError def make_resource(self, name, cls, soft=False, **kw): raise NotImplementedError def del_resource(self, name, soft=False, ref_action='restrict'): raise NotImplementedError def copy_resource(self, source, target, check_if_authorized=True): raise NotImplementedError def move_resource(self, source, target, check_if_authorized=True): raise NotImplementedError def traverse_resources(self): yield self def _get_names(self): return [] def get_names(self, path='.'): resource = self.get_resource(path) return resource._get_names() ####################################################################### # API / Views ####################################################################### def get_default_view_name(self): views = self.class_views if not views: return None context = get_context() for view_name in views: view = getattr(self, view_name, None) if context.is_access_allowed(self, view): return view_name return views[0] def get_view(self, name, query=None): # To define a default view, override this if name is None: name = self.get_default_view_name() if name is None: return None # Explicit view, defined by name view = getattr(self, name, None) if is_prototype(view, ItoolsView): context = get_context() view = view(resource=self, context=context) # bind return view return None def get_context_menus(self): return self.context_menus ######################################################################## # Properties ######################################################################## def check_if_context_exists(self): """ We cannot do: - resource.get_value(xxx) - resource.set_value('name', xxx) if there's no context. """ context = get_context() if context is None: raise ValueError('Error: No context was defined') def get_value(self, name, language=None): field = self.get_field(name) if field is None: msg = 'field {name} is not defined on {class_id}' log_warning(msg.format(name=name, class_id=self.class_id)) return None # Check context self.check_if_context_exists() # Check if field is obsolete if field.obsolete: msg = 'field {name} is obsolete on {class_id}' log_warning(msg.format(name=name, class_id=self.class_id)) # TODO: Use decorator for cache # TODO: Reactivate when ready #cache_key = (name, language) #if self._values.has_key(cache_key): # return self._values[cache_key] if self._brain and field.stored and not is_prototype( field.datatype, Decimal): try: value = self.get_value_from_brain(name, language) except Exception: # FIXME Sometimes we cannot get value from brain # We're tying to debug this problem msg = 'Warning: cannot get value from brain {0} {1}' msg = msg.format(self.abspath, name) print(msg) value = field.get_value(self, name, language) else: value = field.get_value(self, name, language) #self._values[cache_key] = value return value def get_value_from_brain(self, name, language=None): # If brain is loaded & field is stored get value from xapian field = self.get_field(name) brain_value = self._brain.get_value(name, language) if type(brain_value) is datetime: # Fix tzinfo for datetime values context = get_context() value = context.fix_tzinfo(brain_value) else: value = brain_value value = value or field.default if value is None: # Xapian do not index default value value = field.get_value(self, name, language) return value def set_value(self, name, value, language=None, **kw): self.check_if_context_exists() field = self.get_field(name) if field is None: raise ValueError('Field %s do not exist' % name) if field.multilingual and language is None and not isinstance( value, MSG): raise ValueError('Field %s is multilingual' % name) # TODO: Use decorator for cache self.clear_cache(name, language) # Set value return field.set_value(self, name, value, language, **kw) def clear_cache(self, name, language): cache_key = (name, language) if self._values.get(cache_key): del self._values[cache_key] if self._values_title.get(cache_key): del self._values_title[cache_key] self._brain = None def get_value_title(self, name, language=None, mode=None): # TODO: Use decorator for cache # TODO: Reactivate when ready #cache_key = (name, language) #if (self._values_title.has_key(cache_key) and # self._values_title[cache_key].has_key(mode)): # return self._values_title[cache_key][mode] field = self.get_field(name) if field is None: return None value_title = field.get_value_title(self, name, language, mode) #self._values_title.setdefault(cache_key, {})[mode] = value_title return value_title def get_brain_value(self, name): brain = get_context().database.search( PhraseQuery('abspath', str(self.abspath))).get_documents()[0] return getattr(brain, name, None) def get_html_field_body_stream(self, name, language=None): """Utility method, returns the stream for the given html field. """ # 1. Check it is an html-file field field = self.get_field(name) if not is_prototype(field, HTMLFile_Field): raise ValueError, 'expected html-file field' # 2. Get the handler handler = field.get_value(self, name, language) if not handler: handler = field.class_handler() # 3. Get the body body = handler.get_body() if not body: raise ValueError, 'html file does not have a body' return body.get_content_elements() def get_property(self, name, language=None): property = self.metadata.get_property(name, language=language) if property: return property field = self.get_field(name) if field is None: return None default = field.get_default() if field.multiple: return [MetadataProperty(x, None) for x in default] return MetadataProperty(default, None) # XXX Backwards compatibility set_property = set_value def get_page_title(self): return self.get_title() def init_resource(self, **kw): """Return a Metadata object with sensible default values. """ context = get_context() user = context.user now = context.fix_tzinfo(datetime.now()) # UUID self.set_uuid() # Ctime if 'ctime' not in kw: self.set_value('ctime', now) # Mtime if 'mtime' not in kw: self.set_value('mtime', now) # Last author if 'last_author' not in kw and user: self.set_value('last_author', user.name) # Ownership owner = self.get_field('owner', soft=True) if owner and user: self.set_value('owner', str(user.abspath)) # Keyword parameters for name, value in kw.items(): field = self.get_field(name) if field is None: raise ValueError, 'undefined field "%s"' % name if type(value) is dict: for lang in value: field._set_value(self, name, value[lang], lang) else: field._set_value(self, name, value) def set_uuid(self): self.set_value('uuid', uuid4().hex) def update_resource(self, context): """ Method called every time the resource is changed""" pass def load_handlers(self): self.get_handlers() def has_property(self, name, language=None): return self.metadata.has_property(name, language=language) def del_property(self, name): if self.has_property(name): self.reindex() self.metadata.del_property(name) ######################################################################## # Versioning ######################################################################## def get_files_to_archive(self, content=False): metadata = self.metadata.key if content is True: folder = self.handler.key return [metadata, folder] return [metadata] def get_revisions(self, n=None, content=False, author_pattern=None, grep_pattern=None): if self.parent is None and content is True: files = None else: files = self.get_files_to_archive(content) backend = self.database.backend return backend.git_log(files, n, author_pattern, grep_pattern) def get_owner(self): if self.get_field('owner'): return self.get_value('owner') return None def get_share(self): return self.get_value('share') ######################################################################## # Indexing ######################################################################## def to_text(self): """This function must return: 1) An unicode text. or 2) A dict in a multilingual context: {'fr': u'....', 'en': u'....' ....} """ return None def get_catalog_values(self): values = {} # Step 1. Automatically index fields root = self.get_root() languages = root.get_value('website_languages') for name, field in self.get_fields(): if not field.indexed and not field.stored: continue if field.multilingual: value = {} for language in languages: value[language] = field.get_value(self, name, language) values[name] = value else: values[name] = field.get_value(self, name) # Step 2. Index non-metadata properties # Path related fields abspath = self.abspath values['abspath'] = str(abspath) n = len(abspath) values['abspath_depth'] = n if n: values['parent_paths'] = [str(abspath[:i]) for i in range(n)] values['name'] = self.name # Class related fields values['format'] = self.metadata.format values['base_classes'] = self.get_base_classes() values['class_version'] = class_version_to_date(self.metadata.version) # Links to other resources values['owner'] = self.get_owner() values['share'] = self.get_share() values['links'] = list(self.get_links()) values['onchange_reindex'] = self.get_onchange_reindex() # Full text indexation (not available in icms-init.py FIXME) context = get_context() server = context.server if server and server.index_text: try: values['text'] = self.to_text() except Exception: log = 'Indexation failed: %s' % abspath log_warning(log, domain='ikaaro') # Time events for the CRON reminder, payload = self.next_time_event() values['next_time_event'] = reminder if payload: values['next_time_event_payload'] = dumps(payload) else: values['next_time_event_payload'] = None # Ok return values def get_onchange_reindex(self): return None def get_base_classes(self): l = [] for cls in self.__class__.__mro__: class_id = getattr(cls, 'class_id', None) if class_id: l.append(class_id) return l ####################################################################### # Time events ####################################################################### def next_time_event(self): return None, None def time_event(self, payload): raise NotImplementedError ####################################################################### # API ####################################################################### def rename_handlers(self, new_name): """Consider we want to rename this resource to the given 'new_name', return the old a new names for all the attached handlers (except the metadata). This method is required by the "move_resource" method. """ langs = self.get_resource('/').get_value('website_languages') aux = [] for field_name in self.fields: field = self.get_field(field_name) if field and is_prototype(field, File_Field): old = '%s.%s' % (self.name, field_name) new = '%s.%s' % (new_name, field_name) if field.multilingual: for language in langs: aux.append(('%s.%s' % (old, language), '%s.%s' % (new, language))) else: aux.append((old, new)) return aux def _on_move_resource(self, source): """This method updates the links from/to other resources. It is called when the resource has been moved and/or renamed. This method is called by 'Database._before_commit', the 'source' parameter is the place the resource has been moved from. """ # (1) Update links to other resources self.update_incoming_links(Path(source)) # (2) Update resources that link to me database = self.database target = self.abspath query = PhraseQuery('links', source) results = database.search(query).get_documents() for result in results: path = result.abspath path = database.resources_old2new.get(path, path) resource = self.get_resource(path) resource.update_links(source, target) def get_links(self): # Automatically from the fields languages = self.get_resource('/').get_value('website_languages') links = set() for field_name in self.fields: field = self.get_field(field_name) if field: field.get_links(links, self, field_name, languages) # Support for dynamic models class_id = self.metadata.format if class_id[0] == '/': links.add(class_id) # Ok return links def update_links(self, source, target): """The resource identified by 'source' is going to be moved to 'target'. Update our links to it. The parameters 'source' and 'target' are absolute 'Path' objects. """ base = str(self.abspath) old_base = self.database.resources_new2old.get(base, base) old_base = Path(old_base) new_base = Path(base) languages = self.get_resource('/').get_value('website_languages') for field_name in self.fields: field = self.get_field(field_name) if field: field.update_links(self, field_name, source, target, languages, old_base, new_base) self.reindex() def update_incoming_links(self, source): """Update the relative links coming out from this resource after it was moved, so they are not broken. The old path is in parameter. The new path is "self.abspath". """ languages = self.get_resource('/').get_value('website_languages') for field_name in self.fields: field = self.get_field(field_name) if field: field.update_incoming_links(self, field_name, source, languages) ######################################################################## # Upgrade ######################################################################## def get_next_versions(self): cls_version = self.class_version obj_version = self.metadata.version # Set zero version if the resource does not have a version if obj_version is None: obj_version = '00000000' # Get all the version numbers versions = [] for cls in self.__class__.mro(): for name in cls.__dict__.keys(): if not name.startswith('update_'): continue kk, version = name.split('_', 1) if len(version) != 8: continue try: int(version) except ValueError: continue if version > obj_version and version <= cls_version: versions.append(version) versions.sort() return versions def update(self, version): """ Update the ressource to a new version. :param version: The target version """ # Action getattr(self, 'update_%s' % version)() # If the action removes the resource, we are done metadata = self.metadata if metadata.key is None: return # Update version metadata.set_changed() metadata.version = version # Set resource as changed (to reindex class_version) self.reindex() def change_class_id(self, new_class_id): self.metadata.change_class_id(new_class_id) # Return the changed resource return self.get_resource(self.abspath) def reindex(self): # Set resource as changed (to reindex resource) self.database.change_resource(self) ####################################################################### # Icons ####################################################################### @classmethod def get_class_icon(cls, size=16): return getattr(cls, 'class_icon%s' % size, None) @classmethod def get_resource_icon(cls, size=16): icon = getattr(cls, 'icon%s' % size, None) if icon is None: return cls.get_class_icon(size) return ';icon%s' % size def get_method_icon(self, view, size='16x16', **kw): icon = getattr(view, 'icon', None) if icon is None: return None if callable(icon): icon = icon(self, **kw) return '/ui/ikaaro/icons/%s/%s' % (size, icon) ####################################################################### # User interface ####################################################################### def get_views(self): context = get_context() for name in self.class_views: view_name = name.split('?')[0] view = self.get_view(view_name) if context.is_access_allowed(self, view): yield name, view def get_title(self, language=None): title = self.get_value('title', language=language) if title: return title # Fallback to the resource's name return unicode(self.name) def get_edit_languages(self, context): root = self.get_root() site_languages = root.get_value('website_languages') default = root.get_default_edit_languages() # Can not use context.query[] because edit_language is not necessarily # defined datatype = String(multiple=True, default=default) edit_languages = context.get_query_value('edit_language', datatype) edit_languages = [x for x in edit_languages if x in site_languages] return edit_languages if edit_languages else default ####################################################################### # Cut & Paste Resources ####################################################################### def can_paste(self, source): """Is the source resource can be pasted into myself. Question is "can I handle this type of resource?" """ raise NotImplementedError def can_paste_into(self, target): """Can I be pasted into the given target. Question is "Is this container compatible with myself?" """ # No restriction by default. Functional modules will want to keep # their specific resources for them. return True # Views new_instance = AutoAdd(fields=['title', 'location']) edit = AutoEdit(fields=['title', 'description', 'subject', 'share']) remove = DBResource_Remove() get_file = DBResource_GetFile() get_image = DBResource_GetImage() # Login/Logout login = LoginView() logout = LogoutView() # Popups add_image = DBResource_AddImage() add_link = DBResource_AddLink() add_media = DBResource_AddMedia() # Commit log commit_log = DBResource_CommitLog() changes = DBResource_Changes() # Links backlinks = DBResource_Backlinks() links = DBResource_Links() # Rest (web services) rest_login = Rest_Login() rest_query = Rest_Query() rest_create = Rest_Create() rest_read = Rest_Read() rest_update = Rest_Update() rest_delete = Rest_Delete() rest_schema = Rest_Schema()