OLD | NEW |
1 # Copyright: 2012 MoinMoin:CheerXiao | 1 # Copyright: 2012 MoinMoin:CheerXiao |
2 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details. | 2 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details. |
3 | 3 |
4 """ | 4 """ |
5 MoinMoin - Ticket itemtype | 5 MoinMoin - Ticket itemtype |
6 """ | 6 """ |
7 | 7 |
8 | 8 |
9 from __future__ import absolute_import, division | 9 from __future__ import absolute_import, division |
10 | 10 |
11 import time | 11 import time |
| 12 import datetime |
12 | 13 |
13 from flask import request, abort, redirect, url_for | 14 from flask import request, abort, redirect, url_for |
14 from flask import g as flaskg | 15 from flask import g as flaskg |
| 16 from flask import current_app as app |
15 | 17 |
16 from jinja2 import Markup | 18 from jinja2 import Markup |
17 | 19 |
18 from whoosh.query import Term | 20 from whoosh.query import Term, And |
19 | 21 |
20 from MoinMoin.i18n import L_ | 22 from MoinMoin.i18n import L_ |
21 from MoinMoin.themes import render_template | 23 from MoinMoin.themes import render_template |
22 from MoinMoin.forms import (Form, OptionalText, OptionalMultilineText, SmallNatu
ral, Tags, | 24 from MoinMoin.forms import (Form, OptionalText, OptionalMultilineText, SmallNatu
ral, Tags, |
23 Reference, BackReference, SelectSubmit, Text) | 25 Reference, BackReference, SelectSubmit, Text, File) |
24 from MoinMoin.storage.middleware.protecting import AccessDenied | 26 from MoinMoin.storage.middleware.protecting import AccessDenied |
25 from MoinMoin.constants.keys import (ITEMTYPE, CONTENTTYPE, ITEMID, CURRENT, | 27 from MoinMoin.constants.keys import (ITEMTYPE, CONTENTTYPE, ITEMID, CURRENT, |
26 SUPERSEDED_BY, SUBSCRIPTIONS, DEPENDS_ON, N
AME, SUMMARY, NAMESPACE) | 28 SUPERSEDED_BY, SUBSCRIPTIONS, DEPENDS_ON, |
| 29 NAME, SUMMARY, NAMESPACE, WIKINAME, REFERS_
TO, CONTENT, ACTION_TRASH) |
27 from MoinMoin.constants.contenttypes import CONTENTTYPE_USER | 30 from MoinMoin.constants.contenttypes import CONTENTTYPE_USER |
28 from MoinMoin.items import Item, Contentful, register, BaseModifyForm, get_itemt
ype_specific_tags | 31 from MoinMoin.items import Item, Contentful, register, BaseModifyForm, get_itemt
ype_specific_tags, IndexEntry |
29 from MoinMoin.items.content import NonExistentContent | 32 from MoinMoin.items.content import NonExistentContent |
30 from MoinMoin.util.interwiki import CompositeName | 33 from MoinMoin.util.interwiki import CompositeName |
31 | 34 |
32 | 35 |
33 ITEMTYPE_TICKET = u'ticket' | 36 ITEMTYPE_TICKET = u'ticket' |
34 | 37 |
35 USER_QUERY = Term(CONTENTTYPE, CONTENTTYPE_USER) | 38 USER_QUERY = Term(CONTENTTYPE, CONTENTTYPE_USER) |
36 TICKET_QUERY = Term(ITEMTYPE, ITEMTYPE_TICKET) | 39 TICKET_QUERY = Term(ITEMTYPE, ITEMTYPE_TICKET) |
37 | 40 |
38 Rating = SmallNatural.using(optional=True).with_properties(lower=1, upper=5) | 41 Rating = SmallNatural.using(optional=True).with_properties(lower=1, upper=5) |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
84 id_ = item.meta[ITEMID] | 87 id_ = item.meta[ITEMID] |
85 self['supersedes'].set(Term(SUPERSEDED_BY, id_)) | 88 self['supersedes'].set(Term(SUPERSEDED_BY, id_)) |
86 self['required_by'].set(Term(DEPENDS_ON, id_)) | 89 self['required_by'].set(Term(DEPENDS_ON, id_)) |
87 self['subscribers'].set(Term(SUBSCRIPTIONS, id_)) | 90 self['subscribers'].set(Term(SUBSCRIPTIONS, id_)) |
88 | 91 |
89 | 92 |
90 class TicketForm(BaseModifyForm): | 93 class TicketForm(BaseModifyForm): |
91 meta = TicketMetaForm | 94 meta = TicketMetaForm |
92 backrefs = TicketBackRefForm | 95 backrefs = TicketBackRefForm |
93 message = OptionalMultilineText.using(label=L_("Message")).with_properties(r
ows=8, cols=80) | 96 message = OptionalMultilineText.using(label=L_("Message")).with_properties(r
ows=8, cols=80) |
| 97 data_file = File.using(optional=True, label=L_('Upload file:')) |
94 | 98 |
95 def _load(self, item): | 99 def _load(self, item): |
96 meta = item.prepare_meta_for_modify(item.meta) | 100 meta = item.prepare_meta_for_modify(item.meta) |
97 self['meta'].set(meta, 'duck') | 101 self['meta'].set(meta, 'duck') |
98 # XXX need a more explicit way to test for item creation/modification | 102 # XXX need a more explicit way to test for item creation/modification |
99 if ITEMID in item.meta: | 103 if ITEMID in item.meta: |
100 self['backrefs']._load(item) | 104 self['backrefs']._load(item) |
101 | 105 |
102 | 106 |
103 class TicketSubmitForm(TicketForm): | 107 class TicketSubmitForm(TicketForm): |
104 submit_label = L_("Submit ticket") | 108 submit_label = L_("Submit ticket") |
105 | 109 |
106 def _dump(self, item): | 110 def _dump(self, item): |
107 # initial metadata for Ticket-itemtyped item | 111 # initial metadata for Ticket-itemtyped item |
108 meta = { | 112 meta = { |
109 ITEMTYPE: item.itemtype, | 113 ITEMTYPE: item.itemtype, |
110 # XXX support other markups | 114 # XXX support other markups |
111 CONTENTTYPE: 'text/x.moin.wiki;charset=utf-8', | 115 CONTENTTYPE: 'text/x.moin.wiki;charset=utf-8', |
112 'closed': False, | 116 'closed': False, |
113 } | 117 } |
114 meta.update(self['meta'].value) | 118 meta.update(self['meta'].value) |
115 return meta, message_markup(self['message'].value) | 119 return meta, message_markup(self['message'].value), self['data_file'].va
lue |
116 | 120 |
117 | 121 |
118 class TicketUpdateForm(TicketForm): | 122 class TicketUpdateForm(TicketForm): |
119 submit = SelectSubmit.valued('update', 'update_negate_status') | 123 submit = SelectSubmit.valued('update', 'update_negate_status') |
120 | 124 |
121 def _load(self, item): | 125 def _load(self, item): |
122 super(TicketUpdateForm, self)._load(item) | 126 super(TicketUpdateForm, self)._load(item) |
123 self['submit'].properties['labels'] = { | 127 self['submit'].properties['labels'] = { |
124 'update': L_('Update ticket'), | 128 'update': L_('Update ticket'), |
125 'update_negate_status': (L_('Update & reopen ticket') if item.meta.g
et('closed') | 129 'update_negate_status': (L_('Update & reopen ticket') if item.meta.g
et('closed') |
126 else L_('Update & close ticket')) | 130 else L_('Update & close ticket')) |
127 } | 131 } |
128 | 132 |
129 def _dump(self, item): | 133 def _dump(self, item): |
130 # Since the metadata form for tickets is an incomplete one, we load the | 134 # Since the metadata form for tickets is an incomplete one, we load the |
131 # original meta and update it with those from the metadata editor | 135 # original meta and update it with those from the metadata editor |
132 meta = item.meta_filter(item.prepare_meta_for_modify(item.meta)) | 136 meta = item.meta_filter(item.prepare_meta_for_modify(item.meta)) |
133 meta.update(self['meta'].value) | 137 meta.update(self['meta'].value) |
134 if self['submit'].value == 'update_negate_status': | 138 if self['submit'].value == 'update_negate_status': |
135 meta['closed'] = not meta.get('closed') | 139 meta['closed'] = not meta.get('closed') |
136 | 140 |
137 data = item.content.data_storage_to_internal(item.content.data) | 141 data = item.content.data_storage_to_internal(item.content.data) |
138 message = self['message'].value | 142 message = self['message'].value |
139 if message: | 143 if message: |
140 data += message_markup(message) | 144 data += message_markup(message) |
141 | 145 |
142 return meta, data | 146 return meta, data, self['data_file'].value |
143 | 147 |
144 | 148 |
145 # XXX Ideally we should generate DOM instead of moin wiki source. But | 149 # XXX Ideally we should generate DOM instead of moin wiki source. But |
146 # currently this is not very useful, since | 150 # currently this is not very useful, since |
147 # * DOM cannot be stored directly, it has to be converted to some markup first | 151 # * DOM cannot be stored directly, it has to be converted to some markup first |
148 # * DOM -> markup conversion is only available for moinwiki | 152 # * DOM -> markup conversion is only available for moinwiki |
149 | 153 |
150 # XXX How to do i18n on this? | 154 # XXX How to do i18n on this? |
151 | 155 |
152 def message_markup(message): | 156 def message_markup(message): |
153 return u'''{{{{{{#!wiki tip | 157 return u'''{{{{{{#!wiki tip |
154 %(author)s wrote on <<DateTime(%(timestamp)d)>>: | 158 %(author)s wrote on <<DateTime(%(timestamp)d)>>: |
155 | 159 |
156 %(message)s | 160 %(message)s |
157 }}}}}} | 161 }}}}}} |
158 ''' % dict(author=flaskg.user.name[0], timestamp=time.time(), message=message) | 162 ''' % dict(author=flaskg.user.name[0], timestamp=time.time(), message=message) |
159 | 163 |
160 | 164 |
| 165 def check_itemid(self): |
| 166 # once a ticket has both name and itemid, use itemid |
| 167 if self.meta.get(ITEMID) and self.meta.get(NAME): |
| 168 query = And([Term(WIKINAME, app.cfg.interwikiname), Term(REFERS_TO, self
.meta[NAME])]) |
| 169 revs = flaskg.storage.search(query, limit=None) |
| 170 prefix = self.meta[NAME][0] + '/' |
| 171 for rev in revs: |
| 172 old_names = rev.meta[NAME] |
| 173 for old_name in old_names: |
| 174 file_name = old_name[len(prefix):] |
| 175 try: |
| 176 new_name = self.meta[ITEMID] + '/' + file_name |
| 177 item = Item.create(new_name) |
| 178 item.modify({}, rev.meta[CONTENT], refers_to=self.meta[ITEMI
D]) |
| 179 item = Item.create(old_name) |
| 180 item._save(item.meta, name=old_name, action=ACTION_TRASH) #
delete |
| 181 except AccessDenied: |
| 182 abort(403) |
| 183 |
| 184 |
| 185 def file_upload(self, data_file): |
| 186 contenttype = data_file.content_type # guess by browser, based on file name |
| 187 data = data_file.stream |
| 188 check_itemid(self) |
| 189 if self.meta.get(ITEMID) and self.meta.get(NAME): |
| 190 item_name = self.meta[ITEMID] + '/' + data_file.filename |
| 191 refers_to = self.meta[ITEMID] |
| 192 else: |
| 193 item_name = self.fqname.value + '/' + data_file.filename |
| 194 refers_to = self.fqname.value |
| 195 try: |
| 196 item = Item.create(item_name) |
| 197 item.modify({}, data, contenttype_guessed=contenttype, refers_to=refers_
to) |
| 198 except AccessDenied: |
| 199 abort(403) |
| 200 |
| 201 |
| 202 def get_files(self): |
| 203 check_itemid(self) |
| 204 if self.meta.get(ITEMID) and self.meta.get(NAME): |
| 205 refers_to = self.meta[ITEMID] |
| 206 prefix = self.meta[ITEMID] + '/' |
| 207 else: |
| 208 refers_to = self.fqname.value |
| 209 prefix = self.fqname.value + '/' |
| 210 query = And([Term(WIKINAME, app.cfg.interwikiname), Term(REFERS_TO, refers_t
o)]) |
| 211 revs = flaskg.storage.search(query, limit=None) |
| 212 files = [] |
| 213 for rev in revs: |
| 214 names = rev.meta[NAME] |
| 215 for name in names: |
| 216 relname = name[len(prefix):] |
| 217 file_fqname = CompositeName(rev.meta[NAMESPACE], ITEMID, rev.meta[IT
EMID]) |
| 218 files.append(IndexEntry(relname, file_fqname, rev.meta)) |
| 219 return files |
| 220 |
| 221 |
161 @register | 222 @register |
162 class Ticket(Contentful): | 223 class Ticket(Contentful): |
163 itemtype = ITEMTYPE_TICKET | 224 itemtype = ITEMTYPE_TICKET |
164 display_name = L_('Ticket') | 225 display_name = L_('Ticket') |
165 description = L_('Ticket item') | 226 description = L_('Ticket item') |
166 submit_template = 'ticket/submit.html' | 227 submit_template = 'ticket/submit.html' |
167 modify_template = 'ticket/modify.html' | 228 modify_template = 'ticket/modify.html' |
168 | 229 |
169 def do_show(self, revid): | 230 def do_show(self, revid): |
170 if revid != CURRENT: | 231 if revid != CURRENT: |
171 # TODO When requesting a historical version, show a readonly view | 232 # TODO When requesting a historical version, show a readonly view |
172 abort(403) | 233 abort(403) |
173 else: | 234 else: |
174 return self.do_modify() | 235 return self.do_modify() |
175 | 236 |
176 def do_modify(self): | 237 def do_modify(self): |
177 is_new = isinstance(self.content, NonExistentContent) | 238 is_new = isinstance(self.content, NonExistentContent) |
178 closed = self.meta.get('closed') | 239 closed = self.meta.get('closed') |
179 | 240 |
180 Form = TicketSubmitForm if is_new else TicketUpdateForm | 241 Form = TicketSubmitForm if is_new else TicketUpdateForm |
181 | 242 |
182 if request.method in ['GET', 'HEAD']: | 243 if request.method in ['GET', 'HEAD']: |
183 form = Form.from_item(self) | 244 form = Form.from_item(self) |
184 elif request.method == 'POST': | 245 elif request.method == 'POST': |
185 form = Form.from_request(request) | 246 form = Form.from_request(request) |
186 if form.validate(): | 247 if form.validate(): |
187 meta, data = form._dump(self) | 248 meta, data, data_file = form._dump(self) |
188 try: | 249 try: |
189 self.modify(meta, data) | 250 self.modify(meta, data) |
| 251 file_upload(self, data_file) |
190 except AccessDenied: | 252 except AccessDenied: |
191 abort(403) | 253 abort(403) |
192 else: | 254 else: |
193 try: | 255 try: |
194 fqname = CompositeName(self.meta[NAMESPACE], ITEMID, sel
f.meta[ITEMID]) | 256 fqname = CompositeName(self.meta[NAMESPACE], ITEMID, sel
f.meta[ITEMID]) |
195 except KeyError: | 257 except KeyError: |
196 fqname = self.fqname | 258 fqname = self.fqname |
197 return redirect(url_for('.show_item', item_name=fqname)) | 259 return redirect(url_for('.show_item', item_name=fqname)) |
198 | 260 |
199 # XXX When creating new item, suppress the "foo doesn't exist. Create it
?" dummy content | 261 # XXX When creating new item, suppress the "foo doesn't exist. Create it
?" dummy content |
200 data_rendered = None if is_new else Markup(self.content._render_data()) | 262 data_rendered = None if is_new else Markup(self.content._render_data()) |
201 | 263 |
| 264 files = get_files(self) |
202 suggested_tags = get_itemtype_specific_tags(ITEMTYPE_TICKET) | 265 suggested_tags = get_itemtype_specific_tags(ITEMTYPE_TICKET) |
203 | 266 |
204 return render_template(self.submit_template if is_new else self.modify_t
emplate, | 267 return render_template(self.submit_template if is_new else self.modify_t
emplate, |
205 is_new=is_new, | 268 is_new=is_new, |
206 closed=closed, | 269 closed=closed, |
207 item_name=self.name, | 270 item_name=self.name, |
208 data_rendered=data_rendered, | 271 data_rendered=data_rendered, |
209 form=form, | 272 form=form, |
210 suggested_tags=suggested_tags, | 273 suggested_tags=suggested_tags, |
211 item=self, | 274 item=self, |
| 275 files=files, |
212 ) | 276 ) |
OLD | NEW |