543 id = 'treeitem' |
497 id = 'treeitem' |
544 |
498 |
545 def cell_call(self, row, col): |
499 def cell_call(self, row, col): |
546 self.wview('incontext', self.rset, row=row, col=col) |
500 self.wview('incontext', self.rset, row=row, col=col) |
547 |
501 |
548 |
502 # context specific views ###################################################### |
549 # xml and xml/rss views ####################################################### |
|
550 |
|
551 class XmlView(EntityView): |
|
552 id = 'xml' |
|
553 title = _('xml') |
|
554 templatable = False |
|
555 content_type = 'text/xml' |
|
556 xml_root = 'rset' |
|
557 item_vid = 'xmlitem' |
|
558 |
|
559 def cell_call(self, row, col): |
|
560 self.wview(self.item_vid, self.rset, row=row, col=col) |
|
561 |
|
562 def call(self): |
|
563 """display a list of entities by calling their <item_vid> view""" |
|
564 self.w(u'<?xml version="1.0" encoding="%s"?>\n' % self.req.encoding) |
|
565 self.w(u'<%s size="%s">\n' % (self.xml_root, len(self.rset))) |
|
566 for i in xrange(self.rset.rowcount): |
|
567 self.cell_call(i, 0) |
|
568 self.w(u'</%s>\n' % self.xml_root) |
|
569 |
|
570 |
|
571 class XmlItemView(EntityView): |
|
572 id = 'xmlitem' |
|
573 |
|
574 def cell_call(self, row, col): |
|
575 """ element as an item for an xml feed """ |
|
576 entity = self.complete_entity(row, col) |
|
577 self.w(u'<%s>\n' % (entity.e_schema)) |
|
578 for rschema, attrschema in entity.e_schema.attribute_definitions(): |
|
579 attr = rschema.type |
|
580 try: |
|
581 value = entity[attr] |
|
582 except KeyError: |
|
583 # Bytes |
|
584 continue |
|
585 if value is not None: |
|
586 if attrschema == 'Bytes': |
|
587 from base64 import b64encode |
|
588 value = '<![CDATA[%s]]>' % b64encode(value.getvalue()) |
|
589 elif isinstance(value, basestring): |
|
590 value = xml_escape(value) |
|
591 self.w(u' <%s>%s</%s>\n' % (attr, value, attr)) |
|
592 self.w(u'</%s>\n' % (entity.e_schema)) |
|
593 |
|
594 |
|
595 |
|
596 class XMLRsetView(AnyRsetView): |
|
597 """dumps xml in CSV""" |
|
598 id = 'rsetxml' |
|
599 title = _('xml export') |
|
600 templatable = False |
|
601 content_type = 'text/xml' |
|
602 xml_root = 'rset' |
|
603 |
|
604 def call(self): |
|
605 w = self.w |
|
606 rset, descr = self.rset, self.rset.description |
|
607 eschema = self.schema.eschema |
|
608 labels = self.columns_labels(False) |
|
609 w(u'<?xml version="1.0" encoding="%s"?>\n' % self.req.encoding) |
|
610 w(u'<%s query="%s">\n' % (self.xml_root, html_escape(rset.printable_rql()))) |
|
611 for rowindex, row in enumerate(self.rset): |
|
612 w(u' <row>\n') |
|
613 for colindex, val in enumerate(row): |
|
614 etype = descr[rowindex][colindex] |
|
615 tag = labels[colindex] |
|
616 attrs = {} |
|
617 if '(' in tag: |
|
618 attrs['expr'] = tag |
|
619 tag = 'funccall' |
|
620 if val is not None and not eschema(etype).is_final(): |
|
621 attrs['eid'] = val |
|
622 # csvrow.append(val) # val is eid in that case |
|
623 val = self.view('textincontext', rset, |
|
624 row=rowindex, col=colindex) |
|
625 else: |
|
626 val = self.view('final', rset, displaytime=True, |
|
627 row=rowindex, col=colindex, format='text/plain') |
|
628 w(simple_sgml_tag(tag, val, **attrs)) |
|
629 w(u' </row>\n') |
|
630 w(u'</%s>\n' % self.xml_root) |
|
631 |
|
632 |
|
633 class RssView(XmlView): |
|
634 id = 'rss' |
|
635 title = _('rss') |
|
636 templatable = False |
|
637 content_type = 'text/xml' |
|
638 http_cache_manager = MaxAgeHTTPCacheManager |
|
639 cache_max_age = 60*60*2 # stay in http cache for 2 hours by default |
|
640 |
|
641 def cell_call(self, row, col): |
|
642 self.wview('rssitem', self.rset, row=row, col=col) |
|
643 |
|
644 def call(self): |
|
645 """display a list of entities by calling their <item_vid> view""" |
|
646 req = self.req |
|
647 self.w(u'<?xml version="1.0" encoding="%s"?>\n' % req.encoding) |
|
648 self.w(u'''<rdf:RDF |
|
649 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
|
650 xmlns:dc="http://purl.org/dc/elements/1.1/" |
|
651 xmlns="http://purl.org/rss/1.0/" |
|
652 >''') |
|
653 self.w(u' <channel rdf:about="%s">\n' % html_escape(req.url())) |
|
654 self.w(u' <title>%s RSS Feed</title>\n' % html_escape(self.page_title())) |
|
655 self.w(u' <description>%s</description>\n' % html_escape(req.form.get('vtitle', ''))) |
|
656 params = req.form.copy() |
|
657 params.pop('vid', None) |
|
658 self.w(u' <link>%s</link>\n' % html_escape(self.build_url(**params))) |
|
659 self.w(u' <items>\n') |
|
660 self.w(u' <rdf:Seq>\n') |
|
661 for entity in self.rset.entities(): |
|
662 self.w(u' <rdf:li resource="%s" />\n' % html_escape(entity.absolute_url())) |
|
663 self.w(u' </rdf:Seq>\n') |
|
664 self.w(u' </items>\n') |
|
665 self.w(u' </channel>\n') |
|
666 for i in xrange(self.rset.rowcount): |
|
667 self.cell_call(i, 0) |
|
668 self.w(u'</rdf:RDF>') |
|
669 |
|
670 |
|
671 class RssItemView(EntityView): |
|
672 id = 'rssitem' |
|
673 date_format = '%%Y-%%m-%%dT%%H:%%M%+03i:00' % (timezone / 3600) |
|
674 |
|
675 def cell_call(self, row, col): |
|
676 entity = self.complete_entity(row, col) |
|
677 self.w(u'<item rdf:about="%s">\n' % html_escape(entity.absolute_url())) |
|
678 self._marker('title', entity.dc_long_title()) |
|
679 self._marker('link', entity.absolute_url()) |
|
680 self._marker('description', entity.dc_description()) |
|
681 self._marker('dc:date', entity.dc_date(self.date_format)) |
|
682 if entity.creator: |
|
683 self.w(u'<author>') |
|
684 self._marker('name', entity.creator.name()) |
|
685 email = entity.creator.get_email() |
|
686 if email: |
|
687 self._marker('email', email) |
|
688 self.w(u'</author>') |
|
689 self.w(u'</item>\n') |
|
690 |
|
691 def _marker(self, marker, value): |
|
692 if value: |
|
693 self.w(u' <%s>%s</%s>\n' % (marker, html_escape(value), marker)) |
|
694 |
|
695 |
|
696 class CSVMixIn(object): |
|
697 """mixin class for CSV views""" |
|
698 templatable = False |
|
699 content_type = "text/comma-separated-values" |
|
700 binary = True # avoid unicode assertion |
|
701 csv_params = {'dialect': 'excel', |
|
702 'quotechar': '"', |
|
703 'delimiter': ';', |
|
704 'lineterminator': '\n'} |
|
705 |
|
706 def set_request_content_type(self): |
|
707 """overriden to set a .csv filename""" |
|
708 self.req.set_content_type(self.content_type, filename='cubicwebexport.csv') |
|
709 |
|
710 def csvwriter(self, **kwargs): |
|
711 params = self.csv_params.copy() |
|
712 params.update(kwargs) |
|
713 return UnicodeCSVWriter(self.w, self.req.encoding, **params) |
|
714 |
|
715 |
|
716 class CSVRsetView(CSVMixIn, AnyRsetView): |
|
717 """dumps rset in CSV""" |
|
718 id = 'csvexport' |
|
719 title = _('csv export') |
|
720 |
|
721 def call(self): |
|
722 writer = self.csvwriter() |
|
723 writer.writerow(self.columns_labels()) |
|
724 rset, descr = self.rset, self.rset.description |
|
725 eschema = self.schema.eschema |
|
726 for rowindex, row in enumerate(rset): |
|
727 csvrow = [] |
|
728 for colindex, val in enumerate(row): |
|
729 etype = descr[rowindex][colindex] |
|
730 if val is not None and not eschema(etype).is_final(): |
|
731 # csvrow.append(val) # val is eid in that case |
|
732 content = self.view('textincontext', rset, |
|
733 row=rowindex, col=colindex) |
|
734 else: |
|
735 content = self.view('final', rset, |
|
736 displaytime=True, format='text/plain', |
|
737 row=rowindex, col=colindex) |
|
738 csvrow.append(content) |
|
739 writer.writerow(csvrow) |
|
740 |
|
741 |
|
742 class CSVEntityView(CSVMixIn, EntityView): |
|
743 """dumps rset's entities (with full set of attributes) in CSV""" |
|
744 id = 'ecsvexport' |
|
745 title = _('csv entities export') |
|
746 |
|
747 def call(self): |
|
748 """ |
|
749 the generated CSV file will have a table per entity type |
|
750 found in the resultset. ('table' here only means empty |
|
751 lines separation between table contents) |
|
752 """ |
|
753 req = self.req |
|
754 rows_by_type = {} |
|
755 writer = self.csvwriter() |
|
756 rowdef_by_type = {} |
|
757 for index in xrange(len(self.rset)): |
|
758 entity = self.complete_entity(index) |
|
759 if entity.e_schema not in rows_by_type: |
|
760 rowdef_by_type[entity.e_schema] = [rs for rs, at in entity.e_schema.attribute_definitions() |
|
761 if at != 'Bytes'] |
|
762 rows_by_type[entity.e_schema] = [[display_name(req, rschema.type) |
|
763 for rschema in rowdef_by_type[entity.e_schema]]] |
|
764 rows = rows_by_type[entity.e_schema] |
|
765 rows.append([entity.printable_value(rs.type, format='text/plain') |
|
766 for rs in rowdef_by_type[entity.e_schema]]) |
|
767 for etype, rows in rows_by_type.items(): |
|
768 writer.writerows(rows) |
|
769 # use two empty lines as separator |
|
770 writer.writerows([[], []]) |
|
771 |
|
772 |
|
773 ## Work in progress ########################################################### |
|
774 |
|
775 class SearchForAssociationView(EntityView): |
|
776 """view called by the edition view when the user asks |
|
777 to search for something to link to the edited eid |
|
778 """ |
|
779 id = 'search-associate' |
|
780 __select__ = (one_line_rset() & match_search_state('linksearch') |
|
781 & non_final_entity()) |
|
782 |
|
783 title = _('search for association') |
|
784 |
|
785 def cell_call(self, row, col): |
|
786 rset, vid, divid, paginate = self.filter_box_context_info() |
|
787 self.w(u'<div id="%s">' % divid) |
|
788 self.pagination(self.req, rset, w=self.w) |
|
789 self.wview(vid, rset, 'noresult') |
|
790 self.w(u'</div>') |
|
791 |
|
792 @cached |
|
793 def filter_box_context_info(self): |
|
794 entity = self.entity(0, 0) |
|
795 role, eid, rtype, etype = self.req.search_state[1] |
|
796 assert entity.eid == typed_eid(eid) |
|
797 # the default behaviour is to fetch all unrelated entities and display |
|
798 # them. Use fetch_order and not fetch_unrelated_order as sort method |
|
799 # since the latter is mainly there to select relevant items in the combo |
|
800 # box, it doesn't give interesting result in this context |
|
801 rql = entity.unrelated_rql(rtype, etype, role, |
|
802 ordermethod='fetch_order', |
|
803 vocabconstraints=False) |
|
804 rset = self.req.execute(rql, {'x' : entity.eid}, 'x') |
|
805 #vid = vid_from_rset(self.req, rset, self.schema) |
|
806 return rset, 'list', "search-associate-content", True |
|
807 |
|
808 |
|
809 class OutOfContextSearch(EntityView): |
|
810 id = 'outofcontext-search' |
|
811 def cell_call(self, row, col): |
|
812 entity = self.entity(row, col) |
|
813 erset = entity.as_rset() |
|
814 if self.req.match_search_state(erset): |
|
815 self.w(u'<a href="%s" title="%s">%s</a> <a href="%s" title="%s">[...]</a>' % ( |
|
816 html_escape(linksearch_select_url(self.req, erset)), |
|
817 self.req._('select this entity'), |
|
818 html_escape(entity.view('textoutofcontext')), |
|
819 html_escape(entity.absolute_url(vid='primary')), |
|
820 self.req._('view detail for this entity'))) |
|
821 else: |
|
822 entity.view('outofcontext', w=self.w) |
|
823 |
|
824 |
|
825 class EditRelationView(EntityView): |
|
826 """Note: This is work in progress |
|
827 |
|
828 This view is part of the edition view refactoring. |
|
829 It is still too big and cluttered with strange logic, but it's a start |
|
830 |
|
831 The main idea is to be able to call an edition view for a specific |
|
832 relation. For example : |
|
833 self.wview('editrelation', person_rset, rtype='firstname') |
|
834 self.wview('editrelation', person_rset, rtype='works_for') |
|
835 """ |
|
836 id = 'editrelation' |
|
837 |
|
838 __select__ = match_form_params('rtype') |
|
839 |
|
840 # TODO: inlineview, multiple edit, (widget view ?) |
|
841 def cell_call(self, row, col, rtype=None, role='subject', targettype=None, |
|
842 showlabel=True): |
|
843 self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') ) |
|
844 entity = self.entity(row, col) |
|
845 rtype = self.req.form.get('rtype', rtype) |
|
846 showlabel = self.req.form.get('showlabel', showlabel) |
|
847 assert rtype is not None, "rtype is mandatory for 'edirelation' view" |
|
848 targettype = self.req.form.get('targettype', targettype) |
|
849 role = self.req.form.get('role', role) |
|
850 category = entity.rtags.get_category(rtype, targettype, role) |
|
851 if category in ('primary', 'secondary') or self.schema.rschema(rtype).is_final(): |
|
852 if hasattr(entity, '%s_format' % rtype): |
|
853 formatwdg = entity.get_widget('%s_format' % rtype, role) |
|
854 self.w(formatwdg.edit_render(entity)) |
|
855 self.w(u'<br/>') |
|
856 wdg = entity.get_widget(rtype, role) |
|
857 if showlabel: |
|
858 self.w(u'%s' % wdg.render_label(entity)) |
|
859 self.w(u'%s %s %s' % |
|
860 (wdg.render_error(entity), wdg.edit_render(entity), |
|
861 wdg.render_help(entity),)) |
|
862 else: |
|
863 self._render_generic_relation(entity, rtype, role) |
|
864 |
|
865 def _render_generic_relation(self, entity, relname, role): |
|
866 text = self.req.__('add %s %s %s' % (entity.e_schema, relname, role)) |
|
867 # pending operations |
|
868 operations = self.req.get_pending_operations(entity, relname, role) |
|
869 if operations['insert'] or operations['delete'] or 'unfold' in self.req.form: |
|
870 self.w(u'<h3>%s</h3>' % text) |
|
871 self._render_generic_relation_form(operations, entity, relname, role) |
|
872 else: |
|
873 divid = "%s%sreledit" % (relname, role) |
|
874 url = ajax_replace_url(divid, rql_for_eid(entity.eid), 'editrelation', |
|
875 {'unfold' : 1, 'relname' : relname, 'role' : role}) |
|
876 self.w(u'<a href="%s">%s</a>' % (url, text)) |
|
877 self.w(u'<div id="%s"></div>' % divid) |
|
878 |
|
879 |
|
880 def _build_opvalue(self, entity, relname, target, role): |
|
881 if role == 'subject': |
|
882 return '%s:%s:%s' % (entity.eid, relname, target) |
|
883 else: |
|
884 return '%s:%s:%s' % (target, relname, entity.eid) |
|
885 |
|
886 |
|
887 def _render_generic_relation_form(self, operations, entity, relname, role): |
|
888 rqlexec = self.req.execute |
|
889 for optype, targets in operations.items(): |
|
890 for target in targets: |
|
891 self._render_pending(optype, entity, relname, target, role) |
|
892 opvalue = self._build_opvalue(entity, relname, target, role) |
|
893 self.w(u'<a href="javascript: addPendingDelete(\'%s\', %s);">-</a> ' |
|
894 % (opvalue, entity.eid)) |
|
895 rset = rqlexec('Any X WHERE X eid %(x)s', {'x': target}, 'x') |
|
896 self.wview('oneline', rset) |
|
897 # now, unrelated ones |
|
898 self._render_unrelated_selection(entity, relname, role) |
|
899 |
|
900 def _render_pending(self, optype, entity, relname, target, role): |
|
901 opvalue = self._build_opvalue(entity, relname, target, role) |
|
902 self.w(u'<input type="hidden" name="__%s" value="%s" />' |
|
903 % (optype, opvalue)) |
|
904 if optype == 'insert': |
|
905 checktext = '-' |
|
906 else: |
|
907 checktext = '+' |
|
908 rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': target}, 'x') |
|
909 self.w(u"""[<a href="javascript: cancelPending%s('%s:%s:%s')">%s</a>""" |
|
910 % (optype.capitalize(), relname, target, role, |
|
911 self.view('oneline', rset))) |
|
912 |
|
913 def _render_unrelated_selection(self, entity, relname, role): |
|
914 rschema = self.schema.rschema(relname) |
|
915 if role == 'subject': |
|
916 targettypes = rschema.objects(entity.e_schema) |
|
917 else: |
|
918 targettypes = rschema.subjects(entity.e_schema) |
|
919 self.w(u'<select onselect="addPendingInsert(this.selected.value);">') |
|
920 for targettype in targettypes: |
|
921 unrelated = entity.unrelated(relname, targettype, role) # XXX limit |
|
922 for rowindex, row in enumerate(unrelated): |
|
923 teid = row[0] |
|
924 opvalue = self._build_opvalue(entity, relname, teid, role) |
|
925 self.w(u'<option name="__insert" value="%s>%s</option>' |
|
926 % (opvalue, self.view('text', unrelated, row=rowindex))) |
|
927 self.w(u'</select>') |
|
928 |
|
929 |
503 |
930 class TextSearchResultView(EntityView): |
504 class TextSearchResultView(EntityView): |
931 """this view is used to display full-text search |
505 """this view is used to display full-text search |
932 |
506 |
933 It tries to highlight part of data where the search word appears. |
507 It tries to highlight part of data where the search word appears. |